Context

这个故事从三年前开始说起.

那时老师让我搞Android的一个防伪项目(精益防伪),就是那个用Android的NFC功能来鉴别一个酒是否是假冒伪劣的,也是那个项目让我认识了卢师兄(在华为可厉害了呢).

那时的我只用过http来进行网络请求,没有用过socket来进行数据交互.

由于项目需要,那时候就临时在Android上搞了一波socket通信.

第一次做Android就要碰Socket还是蛮困难的(虽然我已经搞了两年iOS,但是iOS和Android毕竟还是不一样的,而且我还是个孩子),因为界面没做过,java也不是很熟悉,甚至连Android的手机都没有用过. 由于时间比较紧张,所以界面就请望神帮我画了,我去写了数据处理和Socket通信. 那也是第一次我知道了Android上面有很严格的线程限制. 在iOS上的主线程中调用网络,会堵塞,卡在那儿,等到请求完了,就会恢复;不过在android上面,主线程碰一下网络竟然就异常了~(真凶)

Socket

Socket其实不应该和http分开说,因为http只是socket的一种扩展罢了,要进行网络通信,总是离不开Socket的.http只不过是在socket上面进行了很多复杂的封装罢了(很复杂).

socket,翻译过来就是,插座.打个比方就是,我要和你收发数据,就把数据线插到你的插孔里面,然后我们就在这根线里面交流.一个网络节点可以有很多的插座孔,所以也就可以同时和很多很多人一起交流信息.而http是插上了插座,发一句话,收一句话,然后就把插头拔掉了(连接结束了)好处就是服务器和客户端不用一直维护一个连接,缺点嘛,这是一个短连接,下一次想要进行网络请求还需要把TCP的握手流程走一遍,而且是单向操作(只能客户端找服务器麻烦,服务器永远无法主动找客户端麻烦)

Now

三年前的旧事讲完了,该说说这次的故事了

这次我们的产品中会涉及用到socket传输文件,这比三年前那个要求更高了一些,因为文件还是蛮大的,之前每次的数据也就十几个字节(128位)(NFC标签儿本来也存不了多少内容)

Server

先从服务器开始说起吧!由于这个socket传输只会一对一的传输,所以这个服务器根根本本不用考虑并发和异步什么的东西,所以

就用Python做个简单的socket服务器吧!

code
import threading
import SocketServer
import datetime
import time

BUFFER_SIZE = 4096


class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
    t = None
    wantDisconnect = False

    def handle(self):
        if self.t is None:
            self.t = datetime.datetime.now()
            time_interval = 0
        else:
            time_interval = (datetime.datetime.now() - self.t).seconds
        while time_interval < 60 and not self.wantDisconnect:
            data = self.request.recv(BUFFER_SIZE)
            if data:
                self.t = datetime.datetime.now()
                print "prepare to send file"
                f = open(str(data), 'rb')
                l = f.read(BUFFER_SIZE)
                while l:
                    self.request.send(l)
                    print("sending (%x)", l)
                    l = f.read(BUFFER_SIZE)
                    time.sleep(1/20)
                time.sleep(1)
                self.request.sendall("\nsending ok")
                f.close()
            else:
                print threading.currentThread().name, " no data"
                break

    def setup(self):
        print threading.currentThread(), " start"
        self.t = datetime.datetime.now()

    def finish(self):
        self.wantDisconnect = True
        print threading.currentThread(), " end"


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


HOST, PORT = "0.0.0.0", 55555

server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
server.serve_forever()

我直接在服务器的文件目录下放了几个图片文件用来做测试的~

这差不多基本上是直接从Python的SocketServer文档中抄来的

一个多线程的socket服务器就是这么方便

其中setup()finish()没什么太大的意思,反正就是又一个socket连接进来了和对方主动断开后会做的事情

关键的操作都在handle中!

先贴一下官方的解释:

handle()

    This function must do all the work required to service a request. The default implementation does nothing. Several instance attributes are available to it; the request is available as self.request; the client address as self.client_address; and the server instance as self.server, in case it needs access to per-server information.

    The type of self.request is different for datagram or stream services. For stream services, self.request is a socket object; for datagram services, self.request is a pair of string and socket.

就是说有请求进来了,就会在这里中断 ,aka “回调”(但我觉得用中断好像更加形象生动)

self.request将获取到socket实例,有了这个socket实例我们就可以读读写写了

这里的代码是传入的数据代表的是文件,服务器根据文件名,把数据回传回去,就是这么简单,看看效果图

效果图

密集恐惧症了吧?哈哈哈~在服务器把发送的数据打印出来,可以方便调试,(肉眼)检查数据有没有收完收对……

等下,time.sleep(1/20)这个东西有啥子用处?

我担心发送的太快接收端粘包,所以就稍微给一点点儿延迟,缓解一下压力,不过实测下来貌似是我多虑了~

iOS

以我的尿性,客户端必然是从iOS开始搞的~

目标很简单,连接服务器,发个文件名,一个劲的收数据,最后显示效果

iOS效果

略~

Doing

多年前在CocoaPods上面看到了CocoaAsyncSocket,今天终于要动手搞一搞了!好激动

一般大家在github的README.md里面都要把自己的库的用法好好的解释一遍,让访问者眼前一亮,然后送出Star,不过这个库真牛逼,README.md里面装了整整一页的逼!

然后我去wiki里面看,竟然还给了好多个链接,一时半会儿还找不到他的使用方法,不过我看了CommonPitfalls这个板块,我觉得写的很屌,如果我前面的那些关于socket的废话你没看懂的话,可以去这个链接里面再看看.里面讲了不少误区,坑,雷……

好吧,其实它真正的用法在Intro_GCDAsyncSocket里面

教了连接动作,连接结果,读操作,写操作

贴代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    socket=[[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    NSError *error;
    buf=[NSMutableData data];
    [socket connectToHost:@"192.168.199.131" onPort:55555 error:&error];
}

-(IBAction)send_action:(id)sender{
    [socket writeData:[self.commandField.text  dataUsingEncoding:NSASCIIStringEncoding] withTimeout:5.f tag:12345];
}

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"socket is connected");
    [self.sendButton setEnabled:YES];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"[%ld]Did receive Completely %@,(%ld)",tag,data,data.length);
    self.console.text=[NSString stringWithFormat:@"%@\n%@",data,self.console.text];
    UIImage *image=[UIImage imageWithData:data];
    self.imageView.image=image;
}

- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag{
    NSLog(@"[%ld]receive (%lu)*1024 bytes",tag,partialLength);
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    if (tag==12345){
        [sock readDataToData:[@"\nsending ok" dataUsingEncoding:NSASCIIStringEncoding] withTimeout:60.f tag:119];
    }
}

- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock{
    NSLog(@"socket closed");
    [self.sendButton setEnabled:NO];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
    NSLog(@"socket disconnect");
    NSLog(@"buf is %@",buf);
    [self.sendButton setEnabled:NO];
}

这代码好像蛮长的~

其中有partial的回调方法都是接收不完整的时候会做的事儿

Socket本来就是全双工的,就是可以一边儿发送一边儿接收,不过这现在给我做成了个半双工的了,所以我在读取的时候设置了一个结束条件\nsending ok,接收到了这个的时候,就会停止.

Android

android上面我想装逼,不用框架,直接用socket来撸,写的还是蛮痛苦的

先只贴代码,先不做解释了,因为,写的不是特别好……等到优化好了,再写一篇新的,好好解释一番~

先搞个center

public class SocketCenter {
    SocketEntity socket;
    Handler handler = new Handler();
    Thread socket_thread;
    Handler network_handler;
    public void setup() {
        EventBus.getDefault().register(this);
        socket_thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                network_handler=new Handler();
                Looper.loop();
            }
        });
        socket_thread.start();
    }
    public void connect_server() {
        network_handler.post(new Runnable() {
            @Override
            public void run() {
                socket = new SocketEntity("192.168.199.131", 55555, handler);
                socket.connect();
            }
        });
    }
    public void disconnect() {
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(SocketEvent event) {
        if (event.getName().equalsIgnoreCase(EventConstant.Socket_Connected)) {
            network_handler.post(new Runnable() {
                @Override
                public void run() {
                    socket.ask_for_file("img1.jpg", "\nsending ok");
                }
            });
        }
    }
}

再把实例搞出来

public class SocketEntity {
    private Socket socket;
    String address;
    int port;
    BufferedReader reader;
    BufferedWriter writer;
    byte[] temp_data;
    ArrayList<byte[]> received_queue;
    WeakReference<Handler> handler_reference;
    Thread async_thread;
    SocketEntity(String address, int port, Handler handler) {
        socket = new Socket();
        handler_reference = new WeakReference<Handler>(handler);
        this.address = address;
        this.port = port;
        received_queue = new ArrayList<>();
        async_thread = new Thread();
    }
    Socket getSocket() {
        return socket;
    }
    void connect() {
        Log.e("SocketUtil", "Start Connect");
        if (socket != null) {
            if (!socket.isConnected()) {
                try {
                    socket.connect(new InetSocketAddress(address, port), 5000);
                    writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                    reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    SocketEvent event = SocketEvent.connectEventInstance();
                    EventBus.getDefault().post(event);
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-200);
                    EventBus.getDefault().post(event);
                    writer = null;
                    reader = null;
                }
            }
        }
    }
    void disconnect() {
        if (socket != null) {
            if (socket.isConnected()) {
                try {
                    socket.close();
                    writer.close();
                    reader.close();
                    writer = null;
                    reader = null;
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-400);
                    EventBus.getDefault().post(event);
                    if (!socket.isConnected()) {
                        writer = null;
                        reader = null;
                    }
                }
            }
        }
    }
    void ask_for_file(String filename, String endPoint) {
        send_command(filename);
//        receive_data_until(endPoint.getBytes());
        receive_data(endPoint.getBytes());
    }
    private void receive_data(byte[] endpoint) {
        try {
            char[] buf = new char[1024];
            int total = 0;
            boolean isEnd = false;
            String s = "";
            while (!isEnd) {
                int length = reader.read(buf, 0, 1024);
                isEnd = (length == -1);
                if (length > 0) {
                    char[] valid_data = Arrays.copyOfRange(buf, 0, length);
                    String string = String.valueOf(valid_data);
                    s=s+string;
                    isEnd = string.contains(new String(endpoint));
                    if (!isEnd) {
                        total += length;
                    } else {
                        total += length;
                        total -= endpoint.length;
                    }
                    Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(string.getBytes())));
                    Log.e("String is ", string);
                }
            }
            Log.e("Socket_Received", "Total Size is " + total + " bytes");
//            temp_data=s.getBytes();
            temp_data=Arrays.copyOfRange(s.getBytes(),0,total);
            Log.e("Socket_Received", "Total binary Size is " + temp_data.length + " bytes");
            SocketEvent event= SocketEvent.fileReceivedEventInstance(temp_data);
            EventBus.getDefault().post(event);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void receive_data_until(byte[] ends) {
        try {
            byte[] temp = null;
            byte[] temp_buffer = null;
            while (ends != temp) {
                String line = reader.readLine();
                byte[] bytes = line.getBytes();
                Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(bytes)));
                int length = ends.length;
                if (bytes.length >= length) {
                    temp = Arrays.copyOfRange(bytes, (bytes.length - length), bytes.length);
                    temp_buffer = line.getBytes();
                } else if (temp_buffer != null) {
                    int rear = bytes.length;
                    int front = ends.length - rear;
                    if (temp_buffer.length > front) {
                        temp = new byte[ends.length];
                        for (int i = 0; i < front; i++) {
                            temp[i] = temp_buffer[temp_buffer.length - front + i];
                        }
                        for (int i = 0; i < rear; i++) {
                            temp[front + i] = bytes[i];
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void send_command(byte[] command) {
        String cmd = Arrays.toString(command);
        send_command(cmd);
    }
    private void send_command(String command) {
        if (socket != null && socket.isConnected()) {
            try {
                Log.e("SocketUtil", "Send " + command);
                writer.write(command);
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
                SocketEvent event = SocketEvent.failBaseEventInstance(-1);
                EventBus.getDefault().post(event);
            }
        }
    }
}

这里面有很多的问题,不过还是成功的做到了连接,发送指令,接收数据,由于最后BitmapFactory.decodeByteArray存在一些问题,估计和接收的数据有关系,所以没有效果图……(难过)

End

先写这么多了,等我下次优化完了,说不定就会有更深入的理解,更好的代码,更加出色的性能,到时候再贴出来留作纪念!