欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

初识Socket通讯编程(一)

程序员文章站 2023-03-26 17:00:53
一、什么是socket? 当两台计算机需要通信的时候,往往我们使用的都是TCP去实现的,但是并不会直接去操作TCP协议,通常是通过Socket进行tcp通信。Socket是操作系统提供给开发者的一个接口,通过它,就可以实现设备之间的通信。 二、TCP是如何通信的? TCP连接和断开分别会存在3次握手 ......
一、什么是socket?
  当两台计算机需要通信的时候,往往我们使用的都是tcp去实现的,但是并不会直接去操作tcp协议,通常是通过socket进行tcp通信。socket是操作系统提供给开发者的一个接口,通过它,就可以实现设备之间的通信。
 
二、tcp是如何通信的?
  tcp连接和断开分别会存在3次握手/4此握手的过程,并且在此过程中包含了发送数据的长度(接受数据的长度),无容置疑,这个过程是复杂的,这里我们不需要做深入的探讨。如果有兴趣,可以参考此文章,这里详细的解释了tcp通信的过程:
 
三、socket消息的收发
  在java中处理socket的方式有三种:
  1. 传统的io流方式(bio模式),阻塞型;
  2. nio的方式;
  3. aio的方式;
  这里只介绍传统的io流方式的tcp连接,即inputstream和outputstream的方式读取和写入数据。对于长连接,通常情况可能我们如下做:
//<--------------服务端代码-------------------->
public class socketreadlister implements runnable {

    private final int tcpport=9999;
    private serversocket serversocket;

    @override
    public void run() {
        try {
            serversocket = new serversocket(this.tcpport);
            while(true){
                socket socket = serversocket.accept();
                //socket.setsotimeout(5*1000);//设置读取数据超时时间为5s
                new thread(new socketreadthread(socket)).start();
            }
        }catch (exception e){
            e.printstacktrace();
        }
    }

    public static void main(string[] args) throws exception{
        new thread(new socketreadlister()).start();
    }
}

public class socketreadthread implements runnable {
    private socket socket;
    public socketreadthread(socket socket) {
        this.socket = socket;
    }

    @override
    public void run() {
        byte[] data = new byte[1024];
        try {
            inputstream is=socket.getinputstream();
            int length=0;
            int num=is.available();
            while((length = is.read(data)) != -1){
                string result = new string(data);
                system.out.println("数据available:"+num);
                system.out.println("数据:"+result);
                system.out.println("length:" + length);
            }
            system.out.print("结束数据读取:"+length);
        }catch (sockettimeoutexception sockettimeoutexception){
            try {
                thread.sleep(2*1000);
            }catch (exception e) {
                e.printstacktrace();
            }
            run();
        } catch (exception e){
            e.printstacktrace();
            try {
                socket.close();
            }catch (ioexception io){
                io.printstacktrace();
            }
        }
    }
}
//<---------------------客户端代码---------------------------->
public class socketclient implements runnable {
    private final int tcpport=9999;
    private socket socket;
    
    @override
    public void run() {
        string msg = "ab23567787hdhfhhfy";

        byte[] bytemsg = msg.getbytes();
            
        try {
            socket = new socket("127.0.0.1", 9999);
            outputstream out = socket.getoutputstream();
            inputstream inputstream=socket.getinputstream();
            
            out.write(bytemsg);
            thread.sleep(10*1000);
            char[] chars=msg.tochararray();
            string str="";
            /*out.flush();*/
            for(int i=0;i<msg.length();i++) {
                str=chars[i]+"-"+i;
                out.write(str.getbytes());
                thread.sleep(1*1000);
            }
            byte[] bytes=new byte[8];
            while(true) {
                if(inputstream.available()>0) {
                    if(inputstream.read(bytes)!=-1) {
                        system.out.println(new string(bytes));
                    }
                }
                thread.sleep(10*1000);
            }
        } catch (exception e) {
            e.printstacktrace();
            try {
                socket.close();
            } catch (ioexception e2) {
                e2.printstacktrace();
            }
        }
    }

    public static void main(string[] args) {
        new thread(new socketclient()).start();
    }

}
  正如代码中所示,通常情况下我们在while循环中将is.read(data)) != -1作为判断依据,判断是否继续读取,这种情况下,确实可以将数据完整的读取,但是客户端没有传输数据的时候,read()方法开始阻塞,直到有数据时才继续执行后续代码,使得程序挂起。
  为什么会出现这种情况呢?
  在jdk中,关于read()的说明如下:当读取到流的末尾,没有可读数据的时候,read()方法将返回-1,如果没有数据,那么read()将会发生阻塞。因此,在读取文件流的情况下,这样是完全正确的,但是在网络编程的情况下,socket连接不会断开,那么inputstream的read()将永远不会返回-1,程序将读完数据后,继续循环读取然后发生阻塞。
  在inputstream中,提供了available();此方法是非阻塞的,通过它可以初步的判定socket流中是否有数据,并返回一个预估数据长度的值,但是请注意,这里是预估,并不是准确的计算出数据的长度,所以在jdk说明文档中,有提示使用该方法获取的值去声明 byte[]的长度,然后读取数据,这是错误的做法。这样在每次读取数据之前,都可以先判断一下流中是否存在数据,然后再读取,这样就可以避免阻塞造成程序的挂起。代码如下:
while(true){
    if(is.available()>0){
        is.read(data);
    }
}
  说到read(),在inputstream中提供了3个read的重载方法:read()、read(byte[])、read(byte[],int offset,int len);后面两种读取方法都是基于 read()实现的,同样存在阻塞的特性,那么我们可以思考一下,假定byte[]的长度为1024,撇开while,拿read(byte[])一次性读取来说,当另一端发送的数据不足1024个字节时,为什么这个read(byte[])没有发生阻塞?
  关于这个问题,网上有帖子说,这跟inputstream的flush()有关,但经过测试,我不这么认为。我更加认同中所说的那样,tcp握手期间,会传递数据的长度,当读取完数据,read()返回-1,即使此时没有读取到1024个字节数据,剩下的用0填充,这样就能很好的解释这个问题了。
  socket既然时网络通讯用,那么由于各种原因,必然会有网络延迟,造成socket读取超时;socket读取超时时,其连接任然是有效的,因此在处理该异常时不需要关闭连接。以下是代码片段:
if (nrecv < nrecvneed){
    int nsize = 0;
    wsabuf=new byte[nrecvneed-nrecv];
    int readcount = 0; // 已经成功读取的字节的个数
    try {
        while (readcount < wsabuf.length) {
            //thread.sleep(100);//读取之前先将线程休眠,避免循环时,程序占用cpu过高
            try {
                availablenum=inputstream.available();
                if(availablenum>0){
                    readcount += inputstream.read(wsabuf, readcount, (wsabuf.length - readcount));//避免数据读取不完整
                }
            }catch (sockettimeoutexception timeout){
                system.out.println("读取超时,线程执行休眠操作,2秒后再读取");
                thread.sleep(2*1000);
            }
        }
    }catch (exception e){
        system.out.println("读取数据异常");
        e.printstacktrace();
        close();//关闭socket连接
        break;
    }
    nsize=wsabuf.length;
    nrecv+=nsize;
}
  另外,需要补充说明的是,socket.close()方法执行后,只能更改本端的连接状态,不能将该状态通知给对端,也就是说如果服务端或客户端一方执行了close(),另一端并不知道此时连接已经断开了。
  此外,以上代码还存在一个很严重的问题亟待解决,这也是在开发中容易忽视的地方——程序能正常运行,但cpu占用过高;原因如下:
  当readcount < wsabuf.length,即数据还未读取完整时,线程会持续不断的从socket流中读取数据,由于这里使用了inputstream.available()来判断使用需要读取数据,当没有数据传输的时候,此处就变成了一个死循环,说到此处,原因就非常明了了,在计算机运行过程中无论他是单核还是多核,系统获取计算机资源(cpu等)都是按照时间分片的方式进行的,同一时间有且只有一个线程能获取到系统资源,所以当遇到死循环时,系统资源一直得不到释放,因此cpu会越来越高,解决的办法是在循环中对程序进行线程休眠一定时间。