初识Socket通信编程(一)

1、什么是socket?
  当两台计算机须要通讯的时候,每每咱们使用的都是TCP去实现的,可是并不会直接去操做TCP协议,一般是经过Socket进行tcp通讯。Socket是操做系统提供给开发者的一个接口,经过它,就能够实现设备之间的通讯。
 
2、TCP是如何通讯的?
  TCP链接和断开分别会存在3次握手/4此握手的过程,而且在此过程当中包含了发送数据的长度(接受数据的长度),无容置疑,这个过程是复杂的,这里咱们不须要作深刻的探讨。若是有兴趣,能够参考此文章,这里详细的解释了TCP通讯的过程:
 
3、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()有关,但通过测试,我不这么认为。我更加认同 https://ketao1989.github.io/2017/03/29/java-server-in-action/中所说的那样,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会愈来愈高,解决的办法是在循环中对程序进行线程休眠必定时间。
相关文章
相关标签/搜索