2.2.2 TCP服务器端

现在我们将注意力转向如何创建一个服务器端。服务器端的工作是建立一个通信终端,并被动地等待客户端的连接。典型的TCP服务器执行如下两步工作:

1.创建一个ServerSocket实例并指定本地端口。此套接字的功能是侦听该指定端口收到的连接。

2.重复执行:

a.调用ServerSocket的accept()方法以获取下一个客户端连接。基于新建立的客户端连接,创建一个Socket实例,并由accept()方法返回。

b.使用所返回的Socket实例的InputStream和OutputStream与客户端进行通信。

c.通信完成后,使用Socket类的close()方法关闭该客户端套接字连接。

下面的例子TCPEchoServer.java,为我们前面的客户端程序实现了一个回馈服务器。这个服务器程序非常简单,它将一直运行,反复接受连接请求,接收并返回字节信息。直到客户端关闭了连接,它才关闭客户端套接字并停止接收和回馈字节信息。

TCPEchoServer.java

figure_0033_0021

figure_0033_0022

1.应用程序设置和参数解析:第0~12行

2.创建服务器端套接字:第15行

servSock侦听特定端口号上的客户端连接请求,该端口号在构造函数中指定。

3.永久循环,迭代处理新的连接请求:第20~34行

·接受新的连接请求:第21行

ServerSocket实例的唯一目的,是为新的TCP连接请求提供一个新的已连接的Socket实例。当服务器端的已经准备好处理客户端请求时,就调用accept()方法。该方法将阻塞等待,直到有向ServerSocket实例指定端口的新的连接请求到来。(如果新的连接请求到来时,在服务器端的套接字刚创建,而尚未调用accept()方法,那么新的连接将排在一个队列中,在这种情况下调用accept()方法,将立即得到响应,即立即返回客户端套接字。连接的建立细节见第6.4.1节。)ServerSocket类的accept()方法将返回一个Socket实例,该实例已经连接到了远程客户端的套接字,并已准备好读写数据。

·报告已连接的客户端:第23~24行

在新创建的Socket实例中,我们可以查询所连接的客户端的相应地址和端口号。Socket类的getRemoteSocketAddress()方法返回一个包含了客户端地址和端口号的InetSocketAddress实例。InetSocketAddress类的toString()方法以“/<地址>:<端口号>”的形式打印出这些信息。(主机名部分为空,因为该实例只根据地址信息创建。)

·获取套接字的输入输出流:第26~27行

写入这个服务器端套接字的OutputStream的字节信息将从客户端套接字的InputStream中读出,而写入客户端OutputStream的字节信息将从服务器端套接字的InputStream读出。

·接收并复制数据,直到客户端关闭:第30~32行

while循环从输入流中反复读取字节数据(在数据可获得时),并立即将同样的字节返回给输出流,这个过程一直持续到客户端关闭连接。InputStream的read()方法每次获取缓存数组所能放下的最多的字节(在本例中为BUFSIZE个字节),并存入该数组(receiveBuf),同时返回实际读取的字节数。read()方法将阻塞等待,直到有可读数据。如果已经数据已经读完则返回-1,表示客户端关闭了其套接字。在反馈协议中,客户端在接受的字节数与其发送字节数相等时就关闭连接,因此在服务器端最终将从read()方法中收到为-1的返回值。(回顾客户端的情况,从read()方法收到-1返回值表示发生了一个协议错误,因为这种情况只会在服务器端提取关闭连接的时候发生。)

如前文所述,read()方法并不一定要在整个字节数组填满后才返回。实际上它只接收了一个字节时就可以返回。OutputStream类的write()方法将receiveBuf中的recvMsgSize个字节写入套接字。该方法的第二个参数指明了要发送的第一个字节在字节数组中的偏移量。在本例中,0表示从data的最前端传送数据。如果我们使用只以缓存数组为参数的write()方法,那么缓存数组中的所有字节都将被传送,甚至可能包括那些不是从客户端接收来的数据。

·关闭客户端套接字:第33行

关闭套接字连接可以释放与连接相关联的系统资源,同时,这对于服务器端来说也是必需的,因为每一个程序所能够打开的Socket实例数量要受到系统限制。

ServerSocket:创建

figure_0035_0023

一个TCP终端必须与特定的端口号关联,以使客户端能够向该端口号发送连接请求。上面前三个构造函数创建一个TCP端口,此端口与特定的本地端口相关联且已准备好接受(accept)传入的连接请求。端口号的有效范围是0~65 535。(如果端口号被设为0,将选择任意没有使用的端口号)连接队列的大小以及本地地址也可以选择设置。需要注意的是,最大队列长度不是一个严格的限制,也不能用来控制客户端的总数。如果指定了本地地址,该地址就必须是主机的网络接口之一;如果没有指定,套接字将接受指向主机任何IP地址的连接。这将对有多个接口而服务器端只接受其中一个接口连接的主机非常有用。

第四个构造函数能创建一个没有关联任何本地端口的ServerSocket实例。在使用该实例前,必须为其绑定(见下文中的bind()方法)一个端口号。

ServerSocket:操作

figure_0035_0024

bind()方法为套接字关联一个本地端口。每个ServerSocket实例只能与唯一一个端口相关联。如果该实例已经关联了一个端口,或所指定的端口已经被占用,则将抛出IOException异常。

accept()方法为下一个传入的连接请求创建Socket实例,并将已成功连接的Socket实例返回给服务器端套接字。如果没有连接请求等待,accept()方法将阻塞等待,直到有新的连接请求到来或超时。

close()方法关闭套接字。调用该方法后,服务器将拒绝接受传入该套接字的客户端连接请求。

ServerSocket:获取属性

figure_0036_0025

以上方法将返回服务器端套接字的本地地址和端口号。注意,与Socket类不同的是,ServerSocket没有相关联的I/O流。然而,它有另外一些称为选项(option)的属性,并能通过多种方法对选项进行控制。这些内容将在第4.4节介绍。