Socket编程BIO及TCP参数

it2025-09-26  1

Socket

服务端代码 package com.bjmashibing.system.io; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; public class SocketIOPropertites { //server socket listen property: 这些配置不是JVM层级的,是关联到内核的TCP协议栈的一些选项参数。 private static final int RECEIVE_BUFFER = 10; private static final int SO_TIMEOUT = 0; // 服务端的超时时间 private static final boolean REUSE_ADDR = false; private static final int BACK_LOG = 2; // 多少个连接可以被积压 //client socket listen property on server endpoint: private static final boolean CLI_KEEPALIVE = false; private static final boolean CLI_OOB = false; private static final int CLI_REC_BUF = 20; private static final boolean CLI_REUSE_ADDR = false; private static final int CLI_SEND_BUF = 20; private static final boolean CLI_LINGER = true; private static final int CLI_LINGER_N = 0; private static final int CLI_TIMEOUT = 0; // 客户端的超时时间 private static final boolean CLI_NO_DELAY = false; /* StandardSocketOptions.TCP_NODELAY StandardSocketOptions.SO_KEEPALIVE StandardSocketOptions.SO_LINGER StandardSocketOptions.SO_RCVBUF StandardSocketOptions.SO_SNDBUF StandardSocketOptions.SO_REUSEADDR */ public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(); server.bind(new InetSocketAddress(9090), BACK_LOG); server.setReceiveBufferSize(RECEIVE_BUFFER); server.setReuseAddress(REUSE_ADDR); server.setSoTimeout(SO_TIMEOUT); } catch (IOException e) { e.printStackTrace(); } System.out.println("server up use 9090!"); try { while (true) { // System.in.read(); //分水岭: Socket client = server.accept(); //阻塞的,没有 -1 一直卡着不动 accept(4, System.out.println("client port: " + client.getPort()); client.setKeepAlive(CLI_KEEPALIVE); client.setOOBInline(CLI_OOB); client.setReceiveBufferSize(CLI_REC_BUF); client.setReuseAddress(CLI_REUSE_ADDR); client.setSendBufferSize(CLI_SEND_BUF); client.setSoLinger(CLI_LINGER, CLI_LINGER_N); client.setSoTimeout(CLI_TIMEOUT); client.setTcpNoDelay(CLI_NO_DELAY); //client.read //阻塞 没有 -1 0 new Thread( () -> { try { InputStream in = client.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); char[] data = new char[1024]; while (true) { int num = reader.read(data); if (num > 0) { System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num)); } else if (num == 0) { System.out.println("client readed nothing!"); continue; } else { System.out.println("client readed -1..."); System.in.read(); client.close(); break; } } } catch (IOException e) { e.printStackTrace(); } } ).start(); } } catch (IOException e) { e.printStackTrace(); } finally { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } } 客户端代码 package com.bjmashibing.system.io; import java.io.*; import java.net.Socket; public class SocketClient { public static void main(String[] args) { try { Socket client = new Socket("192.168.150.11",9090); client.setSendBufferSize(20); client.setTcpNoDelay(true); // 如果数据量比较小,会不会积攒起来再发,默认是true client.setOOBInLine(true); OutputStream out = client.getOutputStream(); InputStream in = System.in; BufferedReader reader = new BufferedReader(new InputStreamReader(in)); while(true){ String line = reader.readLine(); if(line != null ){ byte[] bb = line.getBytes(); for (byte b : bb) { out.write(b); } } } } catch (IOException e) { e.printStackTrace(); } } } 踪建立连接的过程 启动服务端 开启服务端后,出现了一个对于 9090 的 listen 状态。 TCP 三次握手是走 listen 的,建立连接之后,后面走文件描述符,那就是另外一个环节了,我们后面再讲。 使用jps得到服务端的进程id号:7932 使用lsof -p 7932查看7932端口的文件描述符的分配情况。 启动客户端 客户端启动,进入代码的阻塞等待用户输入逻辑 在服务端抓到了三次握手的包 在服务端看到建立了连接,虽然连接还未被使用。 在客户端进行用户输入之后(服务端也有的阻塞的逻辑,需要回车才能接收client的数据) 继续查看服务端抓包监听 查看服务端的连接状态:双方开辟了资源。即便你程序不要我,我也在内核里有资源用来接收或者等待一类的 服务端输入回车之后 接受到了客户端发过来的数据 刚才的socket连接已经被分配给7932了 lsof 得到了新的文件描述符 6 结论 TCP:面向连接的,可靠的传输协议 Socket:是一个四元组。ip:port ip:port四元组的任何一个元的不同,都可以区分不同的连接。

面试题 1:服务端80端口接收客户端连接之后,是否需要为客户端的连接分配一个随机端口号? 答:不需要。

面试题 2:现在,有一个客户端,有一个服务端, 客户端的ip地址是AIP,程序使用端口号CPORT想要建立连接。 服务端的IP地址是XIP,端口号是XPORT。 现在假设某一个客户端A开了很多连接占满了自己的65535个端口号,那客户端A是否还能与另一个服务端建立建立连接? 答:可以,因为只要能保证四元组唯一即可 注:一台服务器是可以与超过65535个客户端保持长连接的,调优到超过百万连接都没问题,只要四元组唯一就可以了。客户端来了之后,服务端是不需要单独给它开辟一个端口号的。 下面这个图可以说明,无论再多的连接,服务端始终是使用的同一个ip:端口 那么,我们常见的报错“端口号被占用”是什么原因? 我们常见的报错“端口号被占用”实际上是在启动SocketSocket的时候,而不是Socket,两者不是一个概念。如果两个服务使用了相同的端口号,这时如果来了一个数据包,内核无法区分是哪一个服务在LISTEN,不知道要发给哪一个服务了,如下图例子

BIO

服务端代码: import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.time.LocalDateTime; public class ServerSingle { public static void main(String[] args) { byte[] buffer = new byte[1024]; try { ServerSocket serverSocket = new ServerSocket(7001); System.out.println(LocalDateTime.now() + " " + "服务器已启动并监听7001端口"); while (true) { System.out.println(LocalDateTime.now() + " " + "服务器正在等待连接..."); Socket socket = serverSocket.accept(); System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器已接收到连接请求..." + socket.getInetAddress() + " " + socket.getPort()); System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器正在等待数据..."); socket.getInputStream().read(buffer); System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "服务器已经接收到数据"); String content = new String(buffer); System.out.println(LocalDateTime.now() + " " + "客户端端口 " + socket.getPort() + "请求: " + "接收到的数据:" + content); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 客户端代码: import java.io.IOException; import java.net.Socket; import java.util.Scanner; public class Consumer { public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1", 7001); String message = null; Scanner sc = new Scanner(System.in); message = sc.next(); socket.getOutputStream().write(message.getBytes()); socket.close(); sc.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

启动服务端

2020-03-16T12:01:20.766 服务器已启动并监听7001端口 2020-03-16T12:01:20.766 服务器正在等待连接…

通过CMD客户端启动客户端。SocketIO_01\target\classes>java mybio1.Consumer

2020-03-16T12:01:20.766 服务器已启动并监听7001端口 2020-03-16T12:01:20.766 服务器正在等待连接… 2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器已接收到连接请求…/127.0.0.1 55104 2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器正在等待数据…

客户端输入数据:12312312323214

2020-03-16T12:01:20.766 服务器已启动并监听7001端口 2020-03-16T12:01:20.766 服务器正在等待连接… 2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器已接收到连接请求…/127.0.0.1 55104 2020-03-16T12:01:53.862 客户端端口 55104请求: 服务器正在等待数据… 2020-03-16T12:02:35.160 客户端端口 55104请求: 服务器已经接收到数据 2020-03-16T12:02:35.160 客户端端口 55104请求: 接收到的数据:12312312323214 2020-03-16T12:02:35.160 服务器正在等待连接…

结论 从上文的运行结果中我们可以看到,服务器端在启动后,首先需要等待客户端的连接请求(第一次阻塞),如果没有客户端连接,服务端将一直阻塞等待,然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞),如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程。这就是BIO的非常重要的一个特点,BIO会产生两次阻塞,第一次在等待连接时阻塞,第二次在等待数据时阻塞。

在单线程条件下BIO的弱点 在上文中,我们实现了一个简易的服务器,这个简易的服务器是以单线程运行的,其实我们不难看出,当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之,在不考虑多线程的情况下,BIO是无法处理多个客户端请求的。

BIO如何处理并发 在刚才的服务器实现中,我们实现的是单线程版的BIO服务器,不难看出,单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢。其实不难想到,我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。

优化 为了让服务端能够并发处理多个客户端消息,在服务端增加子进程。

最终模型

最新回复(0)