APP下载

基于TCP的Java Socket网络连接过程要点分析

2023-08-26李检辉

电脑知识与技术 2023年20期
关键词:多线程

李检辉

关键词:Java;TCP;三次握手;accept队列;多线程

中图分类号:TP393 文献标识码:A

文章编号:1009-3044(2023)20-0103-03

在网络分层里,基于Java的套接字(Socket) 网络编程应用属于应用层。网络通信双方的兩个应用程序在应用层面上各创建一个Socket,并通过Socket建立一个双向的通信连接以实现应用层面数据的交换[1]。应用程序的Socket并不是直接访问主机通信模块进行数据交换,而是通过调用操作系统提供的Socket API接口来请求数据传输,并通过这个接口选择传输层提供的TCP协议或UDP协议来完成通信。TCP协议要求在真正的数据传输前双方先完成三次“预通信”连接过程,即三次握手,用于确认传输通道是否正常,并建立一条虚连接,然后传送数据,且通信结束时还需要拆除连接[2]。由于UDP是不面向连接的协议,这里不进行分析。

在Java网络编程通信模型里,TCP虚连接请求由客户端(Client)发起,如图1所示。

1) 在第一次握手中,由客户端发起SYN报文,服务端收到该报文后,第一次握手成功;

2) 接着,服务端发起第二次握手,向客户端发送SYN+ACK 报文,客户端收到该报文后,第二次握手成功;

3) 最后,由客户端发起第三次握手,向服务端发送ACK报文,服务端收到该报文,便完成三次握手,双方建立一条TCP连接。

那么,在Java Socket通信程序中,三次握手是否由Socket应用程序完成,三次握手是何时开始,何时结束,程序是如何完成这些步骤的?这些是本文要探讨的主要问题。

1 服务端监听客户端发起连接

Java程序通过类ServerSocket创建服务端,并通过调用bind()方法绑定服务器地址。一台主机可以同时提供多个服务,这些不同服务的IP地址是相同的,因此需要通过不同的端口来区别不同的服务[3]。创建服务器的代码如下:

SocketAddress server_addr =new InetSocketAddress“( 192.168.1.2”,5000);

ServerSocket ss=new ServerSocket();

ss.bind(server_addr,4);

或者直接通过构造方法new ServerSocket(5000,4) 绑定本地端口,其中“192.168.1.2”为服务端的IP 地址,5000为端口。

构造方法及bind()方法中的数值4 是形式参数backlog的实值,表示最大连接数为4。操作系统将绑定某个指定端口的入站连接请求,存储在一个先进先出的队列中。后期,服务器在处理队列中的连接请求时会调用accept()方法,所以,这个队列也被称为ac?cept队列,不同的操作系统对accept队列的长度设定会有所不同。设置backlog值是在应用程序层面设定accept 队列的大小。在SeverSocket 类的源码中, 对backlog设定了默认值50,代码如下:

public void bind(SocketAddress endpoint) throwsIOException {

bind(endpoint, 50);

}

如果在绑定端口时没有给定这个参数值,即调用另一个重载方法ss.bind(server_addr),程序则会将backlog的值自动设定为默认值50。或者绑定端口时给定的backlog4的值小于1时,程序也会将这个值重新设置为50。方法void bind(SocketAddress endpoint,int backlog)中的相关代码如下:

if (backlog < 1)

backlog = 50;

bind()方法在执行时,首先会绑定服务端地址(IP 地址和端口),接着会调用listen()方法传递backlog的值,以设置最大连接数,并开始进入端口监听状态,以等待客户端的连接。此时,服务端操作系统会开始响应到达该端口连接请求。当有客户端连接服务端并完成三次握手后,服务端则会将此连接放入accept队列,等待服务端调用accept()方法从该队列中取出并进行后期通信。如果accept队列中存储的未处理的连接数目达到设定的backlog值,即队列满了,那么,服务端将会拒绝新的客户端的连接请求。

2 客户端触发第一次握手

客户端可以通过如下代码连接服务端:

Socket s = new Socket();

SocketAddress server_addr=new InetSocketAddress“( 192.168.1.2”,5000);

s.connect(server_addr); 或者直接使用Socket 的构造方法连接服务端,例如:

Socket s = new Socket(host_name,port);

从Socket类的源码可知,该构造方法在执行时会自动调用connect()方法连接服务端。

客户端在开始执行connect()方法时,首先触发TCP的第一次握手。如果此时服务端未启动,或者服务端的连接队列满了,那么connect()方法会抛出Socket异常,提示“异常信息:Connection refused: con?nec“t 的错误。如果服务端正常响应,接下来会进行第二次及第三次的握手,当三次握手成功时则connect() 方法会正常返回。

3 三次握手与程序之间的关系

基于TCP协议的Java Socket程序通信的过程可以通过三个层面进行解析,如图2所示应用程序(Cli?ent与Server) 、Socket、操作系统(OS) 之间的关系。三次握手在操作系统层面进行,客户端与服务端之间通过操作系统提供Socket API 接口完成通信连接。

1) 服務端创建ServerSocket对象,调用bind()绑定和监听端口,并创建一个先进先出的accept队列;

2) 客户端创建Socket对象后,通过调用其connect()方法触发了三次握手。连接成功后,系统将该连接放入accept队列。客户端同时通过这个Socket创建后续与服务器通信的输入输出流(in、out) ;

3) 服务端调用accept()方法监听accept队列是否成功连接。如果有,则取出并返回一个与对应客户端通信的Socket对象,并创建与该客户端通信的输入输出流(in、out) ;

4) 在客户端调用connect()方法成功返回后,如果服务端并没有执行accept()方法将这个客户端的请求从accept队列中取出处理,那么客户端并不能真正地和服务端进行应用层面上的通信。但是,客户端已经可以通过Socket建立输入输出流,并开始向服务端发送数据,而服务端操作系统也会在TCP协议层面回复ACK包,并将数据保存在指定的缓冲区中,等待服务端程序的后期处理。

因此,三次握手与accept()方法并无直接关系。通过模拟实验可以验证,在未执行accept()方法的情况下,已经完成三次握手,如图3所示。当accept队列已满时,客户端的第一次握手请求(SYN) 后,会收到RST 包,表示重置连接,即该连接请求被服务端面拒绝,接着客户端会连续尝试发送SYN包,如果仍是收到RST 包,则结束连接请求,如图4所示。只要完成了三次握手,客户端便可以向服务端发送数据,并且这些数据会在服务端的操作系统层面被接收并存储在临时空间。

4 Accept 方法处理要点分析

从服务端资源的安全使用方面考虑,服务端程序需要设定合适的最大连接数。然后,一旦设定了最大连接数,如果程序没有及时调用accept方法对取出ac?cept队列的请求进行处理,则会带来如下两个问题。

1) 如果在短时间内有较多的客户端发起连接请求,而服务端不能够及时地将请求从accept队列取出进行处理,accept队列很快会溢出,致使其他客户端的连接都会被拒绝;

2) 客户端一旦完成了三次握手,则可以通过Socket创建输出流发送数据给服务端,如果服务端程序不及时处理,客户端可能因为没有及时得到回复而进入异常状态,同时服务端相应的缓存会被大量占用。

因此,程序中如何调用accept()方法显得非常重要。在单线程程序中,当服务器调用accept()方法接收到第一个客户请求时,便创建输入输出流,开始与客户端进行通信[4]。在通信结束前,不会再次调用accept()方法接收其他客户的连接请求,而越来越多的未被接收的连接请求就会占满整个accept队列。解决这个问题的方式有两种,一种是应用非阻塞I/O技术,另一种是应用多线程技术。由于非阻塞I/O技术比较复杂,这里不展开分析。

多线程技术可以实现分开执行不同的任务或者分段执行程序代码,从而显著提高程序的运行效率[5]。应用多线程解决问题的关键在于将接收请求与处理请求分离成两个任务。服务端程序的主线程用于执行接收请求任务,循环地调用accept()方法,每当ac?cept()成功返回相应Socket对象时,创建一个新的线程(子线程)并传递Socket对象,启动这个新线程。代码如下:

while (true){

Socket clientSocket = listenSocket.accept();

Thread t = new ClientThread(clientSocket);

t.start();

其中,ClientThread 类为子线程类(class Client?Thread extends Thread)。服务端主线程通过调用Cli?entThread类的构造方法创建子线程,并调用start()方法启动子线程用于执行与客户端通信的任务。这样,主线程启动子线程后便可以快速地返回并进入下一次的循环,继续调用accept()方法接收accept队列中下一个客户端的请求。在这个子线程中,通过传递的Socket对象创建通信的输入输出流,开始与客户端进行数据通信。

应用多线程响应客户端请求时,应考虑服务器的资源状况。由于服务端可以快速地处理accept队列中的请求,将会产生大量的子线程用于各个客户端的通信。如果不做任何控制,过多的子线程也有可能影响系统的性能。因此,最好是应用线程池技术对这些子线程进行管理。

5 结束语

文章从应用程序、Socket和操作系统三个层面分析Java Socket程序,可以看出,TCP三次握手是由程序通过调用操作系统的API接口,并在操作系统内核层面完成的。三次握手是在服务端调用bind()方法后,由客户端调用connect()方法触发完成,服务端的ac?cept()方法只是用于处理已完成的连接请求,并不参与三次握手的过程。但是,如果服务端在与客户端连接成功后,没有及时处理accept队列,也会影响新的客户端的请求,致使三次握手失败。

猜你喜欢

多线程
Java并发工具包对并发编程的优化