在之前的文章《网络编程杂谈之TCP协议》中,我们阐述了TCP协议的基本概念,TCP作为一种可靠的、面向连接的数据传输协议,确保了数据在发送和接收之间的可靠性、顺序性和完整性,特点可以概括如下:

总的来说,TCP协议作用于传输层且适用于大多数需要可靠数据传输的应用程序,如文件传输、上位机通信等,并可以做为其他应用层协议的实现基础,如HTTP、MQTT等。

在代码实现层面,Socket是指一种编程接口(API),不同开发语言基本上都围绕Socket提供了一组用于创建、连接、发送和接收数据的API。下面我们以Java为例,通过java.net包下提供的Socket操作API与 java.io包下提供的IO操作API, 实现一个基本的TCP服务端与客户端的监听、链接并进行消息收发的示例。

服务端

在TCP服务端的实现中我们需要先定义一个ServerSocket对象,并实现对指定IP与端口号的绑定与监听,这里需要注意的是如果一直没有客户端链接,serverSocket.accept会一直处在阻塞状态,一旦有客户端链接事件发生才会向下执行,而服务需要满足与多个客户端进行链接,这也是为什么我们需要一个while (true)去一直轮询执行,因为我们其实是不知道客户端什么时候会链接上来的,下面的读写操作也是同一个道理, 所以为了不影响主线程accept新的客户端,我们把完成链接的Socket开启一个独立的线程或者抛给线程池来处理后续IO读写操作,这是一种典型的BIO(Blocking I/O)即阻塞IO的处理模式。

public class BioServer {  
      
    public static void main(String[] args) throws IOException{  
         
       ExecutorService executor = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),  
             new ThreadPoolExecutor.CallerRunsPolicy());  
       ServerSocket serverSocket = new ServerSocket();  
       serverSocket.bind(new InetSocketAddress("127.0.0.1",9091));//绑定IP地址与端口,定义一个服务端socket,开启监听  
       while (true) {  
          Socket socket = serverSocket.accept();//这里如果没有客户端链接,会一直阻塞等待,一旦有客户端链接就会向下执行
          executor.execute(new BioServerHandler(socket)); //我们把链接Socket抛给线程池
            
       }  
    }  
}

读写操作是通过Socket获取InputStream与OutputStream来完成的,这里的Input与Output是站在你程序的视角来区分的,所以Input是收,Output是发,同理inputStream.read作为IO读操作也是阻塞的,程序只有接受到数据时才会向下执行,由于我们不确定Socket链接的读写操作何时发生,也只能依靠 while (true)轮询执行。

public class BioServerHandler implements Runnable{

    private final Socket socket;
    
    public BioServerHandler(Socket socket) {
       this.socket=socket;
    }
    
    @Override
    public void run() {
       // TODO Auto-generated method stub
       try {
          
          while (true) {
             byte[] rbytes = new byte[1024];
             InputStream inputStream =  socket.getInputStream(); //通过IO输入流接受消息
             int rlength=inputStream.read(rbytes, 0, 1024); //读操作阻塞,一旦接受到数据向下执行并返回接收到的数据长度
             byte[] bytes = new byte[rlength];
             System.arraycopy(rbytes, 0, bytes, 0, rlength);
             String message = new String(bytes);
             System.out.printf("Client: %s%n", message);

             PrintStream writer = new PrintStream(socket.getOutputStream()); //通过IO输出流发送消息
             writer.println("Hello BIO Client");
          }

          
       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
       }
       
    }

}

客户端

public class BioClient {  
    public static void main(String[] args) throws IOException, InterruptedException {  
        Socket socket = new Socket();  
        socket.connect(new InetSocketAddress("127.0.0.1", 9091));  
  
        while (true) {  
            if (!socket.isConnected()) {  
                System.out.print("connecting...");  
                continue;  
            }  
  
            PrintStream writer = new PrintStream(socket.getOutputStream());  
            writer.write("Hello BIO Server".getBytes(StandardCharsets.UTF_8));  
  
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
            String message = reader.readLine();  
            System.out.printf("Server: %s%n", message);  
        }  
  
  
    }  
}

通过上面代码大家能够对Java下TCP网络编程的开发、Socket的操作、BIO(阻塞IO)模型有了基本的了解,当然一个完整的TCP服务或客户端开发需要考虑的问题还有很多,如IO与线程模型、协议的制定、链接的管理、应用层报文粘包、半包等等,后续我们会在此基础上进行进一步的扩展与完善。







关注微信公众号,查看更多技术文章。

网络编程之Socket-LMLPHP

10-03 17:20