【Socket】Java Socket编程基础及深入讲解

2023-11-03

转自:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html

Socket是Java网络编程的基础,了解还是有好处的,

  这篇文章主要讲解Socket的基础编程。Socket用在哪呢,主要用在进程间,网络间通信。本篇比较长,特别做了个目录:

一、Socket通信基本示例

二、消息通信优化

三、服务端优化

四、Socket的其它知识

五、关于Socket的理解

一、Socket通信基本示例

  这种模式是基础,必须掌握,后期对Socket的优化都是在这个基础上的,也是为以后学习NIO做铺垫。

package yiwangzhibujian.onlysend;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);

// server将一直等待连接的到来
System.out.println("server将一直等待连接的到来");
Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
//注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len,"UTF-8"));
}
System.out.println("get message from client: " + sb);
inputStream.close();
socket.close();
server.close();
}
}

 

  服务端监听一个端口,等待连接的到来。

 

package yiwangzhibujian.onlysend;

import java.io.OutputStream;
import java.net.Socket;

public class SocketClient {
  public static void main(String args[]) throws Exception {
    // 要连接的服务端IP地址和端口
    String host = "127.0.0.1"; 
    int port = 55533;
    // 与服务端建立连接
    Socket socket = new Socket(host, port);
    // 建立连接后获得输出流
    OutputStream outputStream = socket.getOutputStream();
    String message="你好  yiwangzhibujian";
    socket.getOutputStream().write(message.getBytes("UTF-8"));
    outputStream.close();
    socket.close();
  }
}

 

  客户端通过ip和端口,连接到指定的server,然后通过Socket获得输出流,并向其输出内容,服务器会获得消息。最终服务端控制台打印如下:

server将一直等待连接的到来
get message from client: 你好  yiwangzhibujian

  通过这个例子应该掌握并了解:

  • Socket服务端和客户端的基本编程
  • 传输编码统一指定,防止乱码

  这个例子做为学习的基本例子,实际开发中会有各种变形,比如客户端在发送完消息后,需要服务端进行处理并返回,如下。

二、消息通信优化

2.1 双向通信,发送消息并接受消息

  这个也是做为Socket编程的基本,应该掌握,例子如下:

package yiwangzhibujian.waitreceive;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
  public static void main(String[] args) throws Exception {
    // 监听指定的端口
    int port = 55533;
    ServerSocket server = new ServerSocket(port);
    
    // server将一直等待连接的到来
    System.out.println("server将一直等待连接的到来");
    Socket socket = server.accept();
    // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[1024];
    int len;
    StringBuilder sb = new StringBuilder();
    //只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
    while ((len = inputStream.read(bytes)) != -1) {
      // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
      sb.append(new String(bytes, 0, len, "UTF-8"));
    }
    System.out.println("get message from client: " + sb);

    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("Hello Client,I get the message.".getBytes("UTF-8"));

    inputStream.close();
    outputStream.close();
    socket.close();
    server.close();
  }
}

 

  与之前server的不同在于,当读取完客户端的消息后,打开输出流,将指定消息发送回客户端,客户端程序为:

 

package yiwangzhibujian.waitreceive;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class SocketClient {
  public static void main(String args[]) throws Exception {
    // 要连接的服务端IP地址和端口
    String host = "127.0.0.1";
    int port = 55533;
    // 与服务端建立连接
    Socket socket = new Socket(host, port);
    // 建立连接后获得输出流
    OutputStream outputStream = socket.getOutputStream();
    String message = "你好  yiwangzhibujian";
    socket.getOutputStream().write(message.getBytes("UTF-8"));
    //通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
    socket.shutdownOutput();
    
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[1024];
    int len;
    StringBuilder sb = new StringBuilder();
    while ((len = inputStream.read(bytes)) != -1) {
      //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
      sb.append(new String(bytes, 0, len,"UTF-8"));
    }
    System.out.println("get message from server: " + sb);
    
    inputStream.close();
    outputStream.close();
    socket.close();
  }
}

 

  客户端也有相应的变化,在发送完消息时,调用关闭输出流方法,然后打开输出流,等候服务端的消息。

2.2 使用场景

  这个模式的使用场景一般用在,客户端发送命令给服务器,然后服务器相应指定的命令,如果只是客户端发送消息给服务器,然后让服务器返回收到消息的消息,这就有点过分了,这就是完全不相信Socket的传输安全性,要知道它的底层可是TCP,如果没有发送到服务器端是会抛异常的,这点完全不用担心。

2.3 如何告知对方已发送完命令

  其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。

2.3.1 通过Socket关闭

  这个是第一章介绍的方式,当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。

  但是这种方式有一些缺点

  • 客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
  • 如果客户端想再次发送消息,需要重现创建Socket连接

2.3.2 通过Socket关闭输出流的方式

  这种方式调用的方法是:

socket.shutdownOutput();

  而不是(outputStream为发送消息到服务端打开的输出流):

outputStream.close();

  如果关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。

  调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。

  这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:

  • 不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接

  这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。

2.3.3 通过约定符号

  这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。

  假如约定单端的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成:

hello yiwangzhibujian
end

  那么服务端响应的读取操作需要进行如下改造:

 

Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
String line;
StringBuilder sb = new StringBuilder();
while ((line = read.readLine()) != null && "end".equals(line)) {
  //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
  sb.append(line);
}

 

  可以看见,服务端不仅判断是否读到了流的末尾,还判断了是否读到了约定的末尾。

  这么做的优缺点如下:

  • 优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)
  • 缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽

  经过了这么多的优化还是有缺点,难道就没有完美的解决方案吗,答案是有的,看接下来的内容。

2.3.4 通过指定长度

  如果你了解一点class文件的结构(后续会写,敬请期待),那么你就会佩服这么设计方式,也就是说我们可以在此找灵感,就是我们可以先指定后续命令的长度,然后读取指定长度的内容做为客户端发送的消息。

  现在首要的问题就是用几个字节指定长度呢,我们可以算一算:

  • 1个字节:最大256,表示256B
  • 2个字节:最大65536,表示64K
  • 3个字节:最大16777216,表示16M
  • 4个字节:最大4294967296,表示4G
  • 依次类推

  这个时候是不是很纠结,最大的当然是最保险的,但是真的有必要选择最大的吗,其实如果你稍微了解一点UTF-8的编码方式(字符编码后续会写,敬请期待),那么你就应该能想到为什么一定要固定表示长度字节的长度呢,我们可以使用变长方式来表示长度的表示,比如:

  • 第一个字节首位为0:即0XXXXXXX,表示长度就一个字节,最大128,表示128B
  • 第一个字节首位为110,那么附带后面一个字节表示长度:即110XXXXX 10XXXXXX,最大2048,表示2K
  • 第一个字节首位为1110,那么附带后面二个字节表示长度:即110XXXXX 10XXXXXX 10XXXXXX,最大131072,表示128K
  • 依次类推

  上面提到的这种用法适合高富帅的程序员使用,一般呢,如果用作命名发送,两个字节就够了,如果还不放心4个字节基本就能满足你的所有要求,下面的例子我们将采用2个字节表示长度,目的只是给你一种思路,让你知道有这种方式来获取消息的结尾:

  服务端程序:

 

package yiwangzhibujian.waitreceive2;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
  public static void main(String[] args) throws Exception {
    // 监听指定的端口
    int port = 55533;
    ServerSocket server = new ServerSocket(port);

    // server将一直等待连接的到来
    System.out.println("server将一直等待连接的到来");
    Socket socket = server.accept();
    // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
    InputStream inputStream = socket.getInputStream();
    byte[] bytes;
    // 因为可以复用Socket且能判断长度,所以可以一个Socket用到底
    while (true) {
      // 首先读取两个字节表示的长度
      int first = inputStream.read();
      //如果读取的值为-1 说明到了流的末尾,Socket已经被关闭了,此时将不能再去读取
      if(first==-1){
        break;
      }
      int second = inputStream.read();
      int length = (first << 8) + second;
      // 然后构造一个指定长的byte数组
      bytes = new byte[length];
      // 然后读取指定长度的消息即可
      inputStream.read(bytes);
      System.out.println("get message from client: " + new String(bytes, "UTF-8"));
    }
    inputStream.close();
    socket.close();
    server.close();
  }
}

 

  此处的读取步骤为,先读取两个字节的长度,然后读取消息,客户端为:

 

package yiwangzhibujian.waitreceive2;

import java.io.OutputStream;
import java.net.Socket;

public class SocketClient {
  public static void main(String args[]) throws Exception {
    // 要连接的服务端IP地址和端口
    String host = "127.0.0.1";
    int port = 55533;
    // 与服务端建立连接
    Socket socket = new Socket(host, port);
    // 建立连接后获得输出流
    OutputStream outputStream = socket.getOutputStream();
    String message = "你好  yiwangzhibujian";
    //首先需要计算得知消息的长度
    byte[] sendBytes = message.getBytes("UTF-8");
    //然后将消息的长度优先发送出去
    outputStream.write(sendBytes.length >>8);
    outputStream.write(sendBytes.length);
    //然后将消息再次发送出去
    outputStream.write(sendBytes);
    outputStream.flush();
    //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法
    message = "第二条消息";
    sendBytes = message.getBytes("UTF-8");
    outputStream.write(sendBytes.length >>8);
    outputStream.write(sendBytes.length);
    outputStream.write(sendBytes);
    outputStream.flush();
    //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法
    message = "the third message!";
    sendBytes = message.getBytes("UTF-8");
    outputStream.write(sendBytes.length >>8);
    outputStream.write(sendBytes.length);
    outputStream.write(sendBytes);    
    
    outputStream.close();
    socket.close();
  }
}

 

  客户端要多做的是,在发送消息之前先把消息的长度发送过去。

  这种事先约定好长度的做法解决了之前提到的种种问题,Redis的Java客户端Jedis就是用这种方式实现的这种方式的缺点:

  • 暂时还没发现

  当然如果是需要服务器返回结果,那么也依然使用这种方式,服务端也是先发送结果的长度,然后客户端进行读取。当然现在流行的就是,长度+类型+数据模式的传输方式。

三、服务端优化

3.1 服务端并发处理能力

  在上面的例子中,服务端仅仅只是接受了一个Socket请求,并处理了它,然后就结束了,但是在实际开发中,一个Socket服务往往需要服务大量的Socket请求,那么就不能再服务完一个Socket的时候就关闭了,这时候可以采用循环接受请求并处理的逻辑:

 

package yiwangzhibujian.multiserver;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
  public static void main(String args[]) throws IOException {
    // 监听指定的端口
    int port = 55533;
    ServerSocket server = new ServerSocket(port);
    // server将一直等待连接的到来
    System.out.println("server将一直等待连接的到来");
    
    while(true){
      Socket socket = server.accept();
      // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
      InputStream inputStream = socket.getInputStream();
      byte[] bytes = new byte[1024];
      int len;
      StringBuilder sb = new StringBuilder();
      while ((len = inputStream.read(bytes)) != -1) {
        // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
        sb.append(new String(bytes, 0, len, "UTF-8"));
      }
      System.out.println("get message from client: " + sb);
      inputStream.close();
      socket.close();
    }
    
  }
}

 

  这种一般也是新手写法,但是能够循环处理多个Socket请求,不过当一个请求的处理比较耗时的时候,后面的请求将被阻塞,所以一般都是用多线程的方式来处理Socket,即每有一个Socket请求的时候,就创建一个线程来处理它。

  不过在实际生产中,创建的线程会交给线程池来处理,为了:

  • 线程复用,创建线程耗时,回收线程慢
  • 防止短时间内高并发,指定线程池大小,超过数量将等待,方式短时间创建大量线程导致资源耗尽,服务挂掉

 

package yiwangzhibujian.threadserver;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SocketServer {
  public static void main(String args[]) throws Exception {
    // 监听指定的端口
    int port = 55533;
    ServerSocket server = new ServerSocket(port);
    // server将一直等待连接的到来
    System.out.println("server将一直等待连接的到来");

    //如果使用多线程,那就需要线程池,防止并发过高时创建过多线程耗尽资源
    ExecutorService threadPool = Executors.newFixedThreadPool(100);
    
    while (true) {
      Socket socket = server.accept();
      
      Runnable runnable=()->{
        try {
          // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
          InputStream inputStream = socket.getInputStream();
          byte[] bytes = new byte[1024];
          int len;
          StringBuilder sb = new StringBuilder();
          while ((len = inputStream.read(bytes)) != -1) {
            // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
            sb.append(new String(bytes, 0, len, "UTF-8"));
          }
          System.out.println("get message from client: " + sb);
          inputStream.close();
          socket.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      };
      threadPool.submit(runnable);
    }

  }
}

 

  使用线程池的方式,算是一种成熟的方式。可以应用在生产中。

3.2 服务端其他属性

  ServerSocket有以下3个属性。

  • SO_TIMEOUT:表示等待客户连接的超时时间。一般不设置,会持续等待。
  • SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。一般不设置,经我的测试没必要,下面会进行详解。
  • SO_RCVBUF:表示接收数据的缓冲区的大小。一般不设置,用系统默认就可以了。

  具体详细的解释可以参照下面。

3.3 性能再次提升

  当现在的性能还不能满足需求的时候,就需要考虑使用NIO,这不是本篇的内容,后续会贴出。

四、Socket的其它知识

  其实如果经常看有关网络编程的源码的话,就会发现Socket还是有很多设置的,可以学着用,但是还是要有一些基本的了解比较好。下面就对Socket的Java API中涉及到的进行简单讲解。首先呢Socket有哪些可以设置的选项,其实在SocketOptions接口中已经都列出来了:

  • int TCP_NODELAY = 0x0001:对此连接禁用 Nagle 算法。
  • int SO_BINDADDR = 0x000F:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
  • int SO_REUSEADDR = 0x04:设置套接字的 SO_REUSEADDR。
  • int SO_BROADCAST = 0x0020:此选项启用和禁用发送广播消息的处理能力。
  • int IP_MULTICAST_IF = 0x10:设置用于发送多播包的传出接口。
  • int IP_MULTICAST_IF2 = 0x1f:设置用于发送多播包的传出接口。
  • int IP_MULTICAST_LOOP = 0x12:此选项启用或禁用多播数据报的本地回送。
  • int IP_TOS = 0x3:此选项为 TCP 或 UDP 套接字在 IP 地址头中设置服务类型或流量类字段。
  • int SO_LINGER = 0x0080:指定关闭时逗留的超时值。
  • int SO_TIMEOUT = 0x1006:设置阻塞 Socket 操作的超时值: ServerSocket.accept(); SocketInputStream.read(); DatagramSocket.receive(); 选项必须在进入阻塞操作前设置才能生效。
  • int SO_SNDBUF = 0x1001:设置传出网络 I/O 的平台所使用的基础缓冲区大小的提示。
  • int SO_RCVBUF = 0x1002:设置传入网络 I/O 的平台所使用基础缓冲区的大小的提示。
  • int SO_KEEPALIVE = 0x0008:为 TCP 套接字设置 keepalive 选项时
  • int SO_OOBINLINE = 0x1003:置 OOBINLINE 选项时,在套接字上接收的所有 TCP 紧急数据都将通过套接字输入流接收。

  上面只是简单介绍了下(来源Java API),下面有对其中的某些的详细讲解,没讲到的后续如果用到会补上。

4.1 客户端绑定端口

  服务端绑定端口是可以理解的,因为要监听指定的端口,但是客户端为什么要绑定端口,说实话我觉得这么做的人有点2,或许有的网络安全策略配置了端口访出,使用户只能使用指定的端口,那么这样的配置也是挺2的,直接说就可以不要留面子。

  当然首先要理解的是,如果没有指定端口的话,Socket会自动选取一个可以用的端口,不用瞎操心的。

  但是你非得指定一个端口也是可以的,做法如下,这时候就不能用Socket的构造方法了,要一步一步来:

 

// 要连接的服务端IP地址和端口
String host = "localhost"; 
int port = 55533;
// 与服务端建立连接
Socket socket = new Socket();
socket.bind(new InetSocketAddress(55534));
socket.connect(new InetSocketAddress(host, port));

 

  这样做就可以了,但是当这个程序执行完成以后,再次执行就会报,端口占用异常:

java.net.BindException: Address already in use: connect

  明明上一个Socket已经关闭了,为什么再次使用还会说已经被占用了呢?如果你是用netstat 命令来查看端口的使用情况:

netstat -n|findstr "55533"
TCP 127.0.0.1:55534 127.0.0.1:55533 TIME_WAIT

  就会发现端口的使用状态为TIME_WAIT,说到这你需要有一点TCP连接的基本常识,建议看《》,这是其中的一点摘抄笔记,或许对理解有一些帮助。

  简单来说,当连接主动关闭后,端口状态变为TIME_WAIT,其他程序依然不能使用这个端口,防止服务端因为超时重新发送的确认连接断开对新连接的程序造成影响。

  TIME_WAIT的时间一般有底层决定,一般是2分钟,还有1分钟和30秒的。

  所以,客户端不要绑定端口,不要绑定端口,不要绑定端口。

4.2 读超时SO_TIMEOUT

  读超时这个属性还是比较重要的,当Socket优化到最后的时候,往往一个Socket连接会一直用下去,那么当一端因为异常导致连接没有关闭,另一方是不应该持续等下去的,所以应该设置一个读取的超时时间,当超过指定的时间后,还没有读到数据,就假定这个连接无用,然后抛异常,捕获异常后关闭连接就可以了,调用方法为:

public void setSoTimeout(int timeout)
throws SocketException

  timeout - 指定的以毫秒为单位的超时值。设置0为持续等待下去。建议根据网络环境和实际生产环境选择。

  这个选项设置的值将对以下操作有影响:

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

4.3 设置连接超时

  这个连接超时和上面说的读超时不一样,读超时是在建立连接以后,读数据时使用的,而连接超时是在进行连接的时候,等待的时间。

4.4 判断Socket是否可用

  当需要判断一个Socket是否可用的时候,不能简简单单判断是否为null,是否关闭,下面给出一个比较全面的判断Socket是否可用的表达式,这是根据Socket自身的一些状态进行判断的,它的状态有:

  • bound:是否绑定
  • closed:是否关闭
  • connected:是否连接
  • shutIn:是否关闭输入流
  • shutOut:是否关闭输出流
socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()&& !socket.isInputShutdown() && !socket.isOutputShutdown()

  建议如此使用,但这只是第一步,保证Socket自身的状态是可用的,但是当连接正常创建后,上面的属性如果不调用本方相应的方法是不会改变的,也就是说如果网络断开、服务器主动断开,Java底层是不会检测到连接断开并改变Socket的状态,所以,真实的检测连接状态还是得通过额外的手段,有两种方式。

4.4.1 自定义心跳包

  双方需要约定,什么样的消息属于心跳包,什么样的消息属于正常消息,假设你看了上面的章节现在说就容易理解了,我们定义前两个字节为消息的长度,那么我们就可以定义第3个字节为消息的属性,可以指定一位为消息的类型,1为心跳,0为正常消息。那么要做的有如下:

  • 客户端发送心跳包
  • 服务端获取消息判断是否是心跳包,若是丢弃
  • 当客户端发送心跳包失败时,就可以断定连接不可用

  具体的编码不再贴出,自己实现即可。

4.4.2 通过发送紧急数据

  Socket自带一种模式,那就是发送紧急数据,这有一个前提,那就是服务端的OOBINLINE不能设置为true,它的默认值是false。

  OOBINLINE的true和false影响了什么:

  • 对客户端没有影响
  • 对服务端,如果设置为true,那么服务端将会捕获紧急数据,这会对接收数据造成混淆,需要额外判断

  发送紧急数据通过调用Socket的方法:

socket.sendUrgentData(0);

  发送数据任意即可,因为OOBINLINE为false的时候,服务端会丢弃掉紧急数据。

  当发送紧急数据报错以后,我们就会知道连接不通了。

4.4.3 真的需要判断连接断开吗

  通过上面的两种方式已经可以判断出连接是否可用,然后我们就可以进行后续操作,可是请大家认真考虑下面的问题:

  1. 发送心跳成功时确认连接可用,当再次发送消息时能保证连接还可用吗?即便中间的间隔很短
  2. 如果连接不可用了,你会怎么做?重新建立连接再次发送数据?还是说单单只是记录日志?
  3. 如果你打算重新建立连接,那么发送心跳包的意义何在?为何不在发送异常时再新建连接?

  如果你认真考虑了上面的问题,那么你就会觉得发送心跳包完全是没有必要的操作,通过发送心跳包来判断连接是否可用是通过捕获异常来判断的。那么我们完全可以在发送消息报出IO异常的时候,在异常中重新发送一次即可,这两种方式的编码有什么不同呢,下面写一写伪代码。

  提前检测连接是否可用:

 

//有一个连接中的socket
Socket socket=...
//要发送的数据
String data="";
try{
    //发送心跳包或者紧急数据,来检测连接的可用性
}catch (Excetption e){
    //打印日志,并重连Socket
    socket=new Socket(host,port);
}
socket.write(data);

 

  直接发送数据,出异常后重新连接再次发送:

 

//有一个连接中的socket
Socket socket=...
//要发送的数据
String data="";
try{
    socket.write(data);
}catch (Excetption e){
    //打印日志,并重连Socket
    socket=new Socket(host,port);
    socket.write(data);
}

 

  通过比较可以发现两种方式的特点,现在简单介绍下:

  • 两种方式均可实现连接断开重新连接并发送
  • 提前检测,再每次发送消息的时候都要检测,影响效率,占用带宽

  希望大家认真考虑,做出自己的选择。

4.5 设置端口重用SO_REUSEADDR 

  首先,创建Socket时,默认是禁止的,设置true有什么作用呢,Java API中是这么介绍的:

关闭 TCP 连接时,该连接可能在关闭后的一段时间内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态)。对于使用已知套接字地址或端口的应用程序而言,如果存在处于超时状态的连接(包括地址和端口),可能不能将套接字绑定到所需的 SocketAddress 上。

使用 bind(SocketAddress) 绑定套接字前启用 SO_REUSEADDR 允许在上一个连接处于超时状态时绑定套接字。

  一般是用在绑定端口的时候使用,但是经过我的测试建议如下:

  • 服务端绑定端口后,关闭服务端,重新启动后不会提示端口占用
  • 客户端绑定端口后,关闭,即便设置ReuseAddress为true,即便能绑定端口,连接的时候还是会报端口占用异常

  综上所述,不建议绑定端口,也没必要设置ReuseAddress,当然ReuseAddress的底层还是和硬件有关系的,或许在你的机器上测试结果和我不一样,若是如此和平台相关性差异这么大配置更是不建议使用了。

4.6 设置关闭等待SO_LINGER

  Java API的介绍是:启用/禁用具有指定逗留时间(以秒为单位)的 SO_LINGER。最大超时值是特定于平台的。 该设置仅影响套接字关闭。 

  大家都是这么说的,当调用Socket的close方法后,没有发送的数据将不再发送,设置这个值的话,Socket会等待指定的时间发送完数据包。说实话,经过我简单的测试,对于一般数据量来说,几十K左右,即便直接关闭Socket的连接,服务端也是可以收到数据的。

  所以对于一般应用没必要设置这个值,当数据量发送过大抛出异常时,再来设置这个值也不晚。那么到达逗留超时值时,套接字将通过 TCP RST 强制性 关闭。启用超时值为零的选项将立即强制关闭。如果指定的超时值大于 65,535,则其将被减少到 65,535。 

4.7 设置发送延迟策略TCP_NODELAY

  一般来说当客户端想服务器发送数据的时候,会根据当前数据量来决定是否发送,如果数据量过小,那么系统将会根据Nagle 算法(暂时还没研究),来决定发送包的合并,也就是说发送会有延迟,这在有时候是致命的,比如说对实时性要求很高的消息发送,在线对战游戏等,即便数据量很小也要求立即发送,如果稍有延迟就会感觉到卡顿,默认情况下Nagle 算法是开启的,所以如果不打算有延迟,最好关闭它。这样一旦有数据将会立即发送而不会写入缓冲区。

  但是对延迟要求不是特别高下还是可以使用的,还是可以提升网络传输效率的。

4.8 设置输出输出缓冲区大小SO_RCVBUF/SO_SNDBUF

  • SO_SNDBUF:发送缓冲
  • SO_RCVBUF:接收缓冲

  默认都是8K,如果有需要可以修改,通过相应的set方法。不建议修改的太小,设置太小数据传输将过于频繁。太大了将会造成消息停留。

  不过我对这个经过测试后有以下结论:

  • 当数据填满缓冲区时,一定会发送
  • 当数据没有填满缓冲区时也会发送,这个算法还是上面说的Nagle 算法

4.9 设置保持连接存活SO_KEEPALIVE

  虽然说当设置连接连接的读超时为0,即无限等待时,Socket不会被主动关闭,但是总会有莫名其妙的软件来检测你的连接是否有数据发送,长时间没有数据传输的连接会被它们关闭掉。

  因此通过设置这个选项为true,可以有如下效果:当2个小时(具体的实现而不同)内在任意方向上都没有跨越套接字交换数据,则 TCP 会自动发送一个保持存活的消息到对面。将会有以下三种响应:

  1. 返回期望的ACK。那么不通知应用程序(因为一切正常),2 小时的不活动时间过后,TCP 将发送另一个探头。
  2. 对面返回RST,表明对面挂了,但是又好了,Socket依然要关闭
  3. 没有响应,说明对面挂了,这时候关闭Socket

  所以对于构建长时间连接的Socket还是配置上SO_KEEPALIVE比较好。

4.10 异常:java.net.SocketException: Connection reset by peer

  这个异常的含义是,我正在写数据的时候,你把连接给关闭了。这个异常在一般正常的编码是不会出现这个异常的,因为用户通常会判断是否读到流的末尾了,读到末尾才会进行关闭操作,如果出现这个异常,那就检查一下判断是否读到流的末尾逻辑是否正确。

 

五、关于Socket的理解

5.1 Socket和TCP/IP

  最近在看《TCP/IP详解 卷1:协议》,关于TCP/IP我觉得讲解的非常详细,我做了点摘抄,可以大致看看,非常建议大家阅读下这本书。通常TCP/IP分为四层:

  也就是说Socket实际上是归属于应用层,使用的事运输层的TCP,使用SocketServer监听的端口,也是可以被Telnet连接的。可以看下面两行代码:

ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();

  在什么情况获取到这个Socket呢,通过理论加测试,结论是在三次握手操作后,系统才会将这个连接交给应用层,ServerSocket 才知道有一个连接过来了。那么系统当接收到一个TCP连接请求后,如果上层还没有接受它(假如SocketServer循环处理Socket,一次一个),那么系统将缓存这个连接请求,既然是缓存那么就是有限度的,书上介绍的是缓存3个,但是经过我的本机测试是50个,也就是说,系统将会为应用层的Socket缓存50和TCP连接(这是和系统底层有关系的),当超过指定数量后,系统将会拒绝连接。

  假如缓存的TCP连接请求发送来数据,那么系统也会缓存这些数据,等待SocketServer获得这个连接的时候一并交给它,这个会在后期学习NIO进行详解。

  换句话说,系统接收TCP连接请求放入缓存队列,而SocketServer从缓存队列获取Socket。

  而上面例子中的为了让服务端知道发送完消息的,关闭输出流的操作:

socket.shutdownOutput();

  其实是对应着四次挥手的第一次:

  也就是上面说的主动关闭,FIN_WAIT_1,这样服务端就能得知客户端发送完消息,此时服务端可以选择关闭连接,也可以选择发送数据后关闭连接:

  这就是TCP所说的半关闭。其实很多知识都是想通的,多学点基础知识还是有必要的。

5.2 Socket和RMI

  RMI基础知识就不多介绍了(后续会写,敬请期待),现在假定你对RMI有所了解,那么一般就会对这两种技术有所比较。或者说在应用的时候就会想用那种技术比较好。

  RMI全称:Remote Method Invocation-远程方法调用,通过名字其实就能对这种技术有个初步的了解。现在我就简单说说我对这两种技术的想法。

  这个待写,等我写完RMI博客的时候补上,那时候会更细致的了解下。

5.3 DatagramSocket与Socket

  这一段涉及到UDP,依然和上面一样,后续会补上。

5.4 拆包和黏包

  使用Socket通信的时候,或多或少都听过拆包和黏包,如果没听过而去贸然编程那么偶尔就会碰到一些莫名其妙的问题,所有有这方面的知识还是比较重要的,至少知道怎么发生,怎么防范。

  现在先简单说明下拆包和黏包的原因:

  • 拆包:当一次发送(Socket)的数据量过大,而底层(TCP/IP)不支持一次发送那么大的数据量,则会发生拆包现象。
  • 黏包:当在短时间内发送(Socket)很多数据量小的包时,底层(TCP/IP)会根据一定的算法(指Nagle)把一些包合作为一个包发送。

  首先可以明确的是,大部分情况下我们是不希望发生拆包和黏包的(如果希望发生,什么都去做即可),那么怎么去避免呢,下面进行详解?

5.4.1 黏包

  首先我们应该正确看待黏包,黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms),那么还是不建议关闭这种优化,如果因为黏包会造成业务上的错误,那么请改正你的服务端读取算法(协议),因为即便不发生黏包,在服务端缓存区也可能会合并起来一起提交给上层,推荐使用长度+类型+数据模式。

  如果不希望发生黏包,那么通过禁用TCP_NODELAY即可,Socket中也有相应的方法:

void setTcpNoDelay(boolean on) 

  通过设置为true即可防止在发送的时候黏包,但是当发送的速率大于读取的速率时,在服务端也会发生黏包,即因服务端读取过慢,导致它一次可能读取多个包。

5.4.2 拆包

  这个问题应该引起重视,在TCP/IP详解中说过:最大报文段长度(MSS)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的 MSS。客户端会尽量满足服务端的要求且不能大于服务端的MSS值,当没有协商时,会使用值536字节。虽然看起来MSS值越大越好,但是考虑到一些其他情况,这个值还是不太好确定,具体详见《TCP/IP详解 卷1:协议》。

  如何应对拆包,其实在上面2.3节已经介绍过了,那就是如何表明发送完一条消息了,对于已知数据长度的模式,可以构造相同大小的数组,循环读取,示例代码如下:

 

int length=1024;//这个是读取的到数据长度,现假定1024
byte[] data=new byte[1024];
int readLength=0;
while(readLength<length){
    int read = inputStream.read(data, readLength, length-readLength);
    readLength+=read;
}

 

  这样当循环结束后,就能读取到完整的一条数据,而不需要考虑拆包了。

 

  单单关于Java的Socket编程已经基本介绍完成了,当然还有更深层次的知识没有涉及到,后续如果能有接触也会写出来,希望我的文章能帮助到有需要的人,如果有什么不对的地方请指出,禁止转载。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Socket】Java Socket编程基础及深入讲解 的相关文章

  • HTTP协议中的短轮询、长轮询、长连接和短连接

    转自 https www cnblogs com Leo wl p 5397265 html 阅读目录 一 引言 二 以前的误解 三 一个疑问 四 长轮询和短轮询 五 长短轮询和长短连接的区别 六 结语 阅读目录 HTTP协议中的短轮询 长
  • 九、网络IO原理-创建ServerSocket的过程

    示例 创建ServerSocker过程 创建ServerSocket并注册端口号8090 ServerSocket server new ServerSocket 8090 while true 循环 final Socket socket
  • SSL工作原理

    转自 https www wosign com Basic howsslwork htm SSL 是一个安全协议 它提供使用 TCP IP 的通信应用程序间的隐私与完整性 因特网的 超文本传输协议 HTTP 使用 SSL 来实现安全的通信
  • Springboot+Netty+Websocket实现消息推送实例

    Springboot Netty Websocket实现消息推送 文章目录 Springboot Netty Websocket实现消息推送 前言 一 引入netty依赖 二 使用步骤 1 引入基础配置类 2 netty服务启动监听器 3
  • 解决java.net.SocketException: No buffer space available (maximum connections reach

    严重 Catalina stop java net SocketException No buffer space available maximum connections reached JVM Bindat java net Plai
  • Java swing + socket 写的一个五子棋网络对战游戏

    自从开始接触Swing以来 就喜欢写写各种管理系统 写多了就萌生了一种类似于实时在线对战的游戏 经过一番构思后就开始着手设计这个网络对战版本的五子棋了 游戏代码包含两部分 常规的C S模式 C代表客户端 S代表服务端 下载代码后先启动服务器
  • MODBUS CRC校验原理及C语言实现

    MODBUS通信协议的CRC校验原理多项式为8005的逆序A001 列01的CRC校验原理 1111111111111111 初始化CRC寄存机 0000000000000001 1111111111111110 异或 0111111111
  • http和Tcp的长连接和短连接

    转自 https www cnblogs com fubaizhaizhuren p 7523374 html http协议和tcp ip 协议的关系 1 http是应用层协议 tcp协议是传输层协议 ip协议是网络协议 2 IP协议主要解
  • 【科普】CRC校验(一)什么是CRC校验?

    目录 CRC 循环冗余校验 CRC 校验码的生成 CRC 的发送方与接收方 发送方 接收方 除法异或运算示意图 CRC 循环冗余校验 CRC Cyclic Redundancy Check 循环冗余检验 是一种用于检测数字数据错误的技术 作
  • 几种获取本机IPv6地址的方法

    如同获取IPv4的地址一般 获取IPv6也是可以通过使用网卡信息来得到IPv6的地址 也可以给一个多播地址发送数据包然后获取IPv6的地址 在IPv4通过iotcl函数是可以获取本主机的IPv4的地址的 但是本楼主使用iotcl函数来获取I
  • Java中的NIO和IO的对比分析

    总的来说 java中的IO和NIO主要有三点区别 IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器 Selectors 1 面向流与面向缓冲 Java NIO和IO之间第一个最大的区别是 IO是面向流的 NIO是面向缓冲区的
  • JAVA socket编程实例

    转载文章 原作者无从考证 感谢作者的无私奉献 事实上网络编程简单的理解就是两台计算机相互通讯数据而已 对于程序员而言 去掌握一种编程接口并使用一种编程模型相对就会显得简单的多了 Java SDK提供一些相对简单的Api来完成这些工作 Soc
  • java Socket 简单实现客户端与服务器间通信(仿聊天室)

    java Socket TCP协议简单实现客户端与服务器间的通信 打赏 执行效果 启动服务器和3个客户端 进行群聊和私聊 执行过程 服务端 首先创建服务器套接字ServerSocket对象并绑定端口 启动服务器 然后ServerSocket
  • C++知识分享: Socket 编程详解,万字长文

    介绍 Socket编程让你沮丧吗 从man pages中很难得到有用的信息吗 你想跟上时代去编Internet相关的程序 但是为你在调用 connect 前的bind 的结构而不知所措 等等 好在我已经将这些事完成了 我将和所有人共享我的知
  • socket,socket.io,mongodb

    Socket 网络上的程序实现双向的数据链接 这个链接的一端成为socket 1 Socket是一个持久链接 2 Socket是双向通信的 Socket VS ajax轮询 ajax轮询 是利用客户端来发送请求 每隔几秒发送一个http请求
  • 项目3:(9)与安川控制器P3000通信模块代码

    1 common h 通信传输的IP地址 typedef struct char serverIp 16 int iPort CONNINFO 定义连接信息数据结构 定义发送数据的结构体 typedef struct double R Bo
  • QT进程间通信 详细介绍

    在QT中 信号和槽的机制取代了这种繁杂的 易崩溃的对象通信机制 信号是当对象状态改变时所发出的 槽是用来接收发射的信号并响应相应事件的类的成员函数 信号和槽的连接是通过connect 函数来实现的 AD 1 QT通信机制 为了更好的实现QT
  • golang之跨语言ipc通信

    1 golang之跨语言ipc通信 文章目录 1 golang之跨语言ipc通信 1 1 unix domain Socket unix域套接字 介绍 1 2 IPC SOCKET通信 1 2 1 函数及地址定义介绍 1 2 2 UNIX
  • 图片详解TCP连接的三次握手,四次断开基本原理

    图片详解TCP连接的三次握手 四次断开 作者 林子 Blog http blog csdn net u013011841 时间 2014年8月 出处 http blog csdn net u013011841 article details
  • 分布式系统详解--基础知识(通信)

    分布式系统详解 基础知识 通信 上一篇文章我们写到了 分布式系统详解 基础知识 线程 简单了解了一下线程的基本概念和线程和分布式的那斩不断理还乱的关系 今天再讲解一下它的另外一个必备知识 通信 进程之间进行通信是分布式的核心 失去了通信 也

随机推荐

  • pycharm: unused import statement错误解决方法

    在pycharm中导入numpy包等 有时候字体都呈现灰色 看提示为 unused import statement 如果出现上述问题 可以从以下几个方面进行尝试 1 Pycharm file 菜单下有Invalidate caches R
  • 数据仓库——分层原理

    目录 一 什么是数据仓库 二 数仓建模的意义 为什么要对数据仓库分层 三 ETL 四 技术架构 五 数仓分层架构 数仓逻辑分层 1 数据引入层 ODS Operational Data Store 又称数据基础层 1 1 数据主要来源 1
  • php mail 权限,PHP mail()函数漏洞总结 · MYZ’s Blog

    漏洞成因 email protected golunski曝光了多个使用PHP mail函数引发命令执行的漏洞 众多使用php内置mail函数的第三方邮件库 如phpmailer SwiftMailer 纷纷中招 这些漏洞的成因和之前曝光的
  • 区块链+保险,落地还有多远?

    以前的区块链还在和炒币紧紧相连 现在 区块链就已经在去币化的路上越走越远 如今 已迅速渗透到保险行业 从风控 运营 再保险等方面影响保险公司的效率和商业模式 随着区块链技术的日益发展 已有不少保险巨头和新兴创业公司开始使用区块链技术来防范保
  • 大数据相关技术学习

    https github com lishuai2016 ls bigdata learn
  • 硬十宝典学习笔记_各种地

    1 定义 GND 指的是电线接地端的简写 代表地线或0线 作为电路或系统基准的等电位点或平面 电路图上和电路板上的GND Ground 代表地线或0线 GND就是公共端的意思 也可以说是地 但这个地并不是真正意义上的地 是出于应用而假设的一
  • iOS学习笔记二——OC代码规范(上)

    文章目录 一 规范格式 1 1 规范的head file格式 1 2 规范的source file格式 二 命名要求 2 1 功能明确 2 2 保持一致性 2 3 使用前缀 2 4 排版惯例 2 5 Class和Protocal 2 6 文
  • html的tab只显示第一个,tab切换+下拉刷新+只有第一个tab有效,第二个tab下拉刷新出来的内容跑到第一个去了 怎么解决 求解答...

    mui table view li overflow hidden margin 1 25rem 3 0 border radius 0 21rem background color FFFFFF i da background url i
  • springboot shardingjdbc与druid数据源冲突解决

    首先看错误信息 cancelling refresh attempt org springframework beans factory BeanCreationException Error creating bean with name
  • JAVA锁机制常见面试题

    synchronized与Lock的区别 synchronied 是关键字 Lock 是接口 synchronied可以修饰方法 代码块 Lock只能修饰代码块 synchronized自动加锁解锁 lock 需要手动加锁解锁 synchr
  • 初级测试小宝典 测试流程,不能复现bug,开发不认为是bug级2020测试点的热点提问的回答

    1 测试流程 1 按阶段划分为 单元测试 集成测试 系统测试 验收测试 2 按是否运行 静态测试 动态测试 3 按是否查看源代码 自盒测试 黑盒测试 功能测试 逻辑功能测试 界面测试 易用性测试 安装测试 兼容性测试 性能测试 一般性能测试
  • java多线程事务处理

    1 自定义事务管理器 Component public class SelfTransactionManager private TransactionStatus transactionStatus 获取事务源 Autowired pri
  • 极力推荐python初学者使用wingIDE

    2010 07 26 极力推荐python初学者使用wingIDE 文章分类 Python编程 wingIDE是一款收费软件 但是它的call tips和auto completion功能实在是太强大了 输入的时候都不用考虑大小写 对于我们
  • VUE 学习笔记(一)开发环境搭建

    1 Visual Studio Code安装及使用 下载地址官网 https code visualstudio com 直接点击下载按钮即可 会根据系统自动下载合适的版本 无需自行选择 2 VSCode 上安装 JavaScript De
  • S7-200 PLC新特性是什么

    更多关于西门子S7 200PLC内容请查看 西门子200系列PLC学习课程大纲 课程筹备中 西门子200PLC能做什么 它可以控制设备 实现自动控制 比如切割机 木雕刻机 写字机 锅炉的自动控制等等 应用广泛 它属于小型的PLC特别适用于一
  • Spring整合JSON

    在使用SpringMVC结合JSON时 需要导入com springsource org codehaus jackson mapper 1 4 2 jar和com springsource org codehaus jackson 1 4
  • C++通讯录管理系统

    C 通讯录管理系统 开发环境 Visual studio 2019 1 系统需求 系统中需要实现的功能如下 添加联系人 向通讯录中添加新人 信息包括 姓名 性别 年龄 联系电话 家庭住址 最多记录10000人 显示联系人 显示通讯录中所有联
  • linux的etc相当于windows,Linux——LDAP(相当于Windows下的AD)

    LDAP 轻量级目录访问服务 通过配置这个服务 我们也可以在linux下面使用目录的形式管理用户 就像windows下面的AD一样 方便我们管理 下面我们就一起来配置openldap服务 本文运行环境 CentOS 5 软件需求 openl
  • Java 的JSON、XML转换方法——目录索引

    原文地址 http blog csdn net ibm hoojo article details 6366429 JSON及XML的Java序列化 反序列化 转换 在WebService Ajax数据传递中 用得比较多 如 在用ExtJS
  • 【Socket】Java Socket编程基础及深入讲解

    转自 https www cnblogs com yiwangzhibujian p 7107785 html Socket是Java网络编程的基础 了解还是有好处的 这篇文章主要讲解Socket的基础编程 Socket用在哪呢 主要用在进