2.2 TCP套接字
Java为TCP协议提供了两个类:Socket类和ServerSocket类。一个Socket实例代表了TCP连接的一端。一个TCP连接(TCP connection)是一条抽象的双向信道,两端分别由IP地址和端口号确定。在开始通信之前,要建立一个TCP连接,这需要先由客户端TCP向服务器端TCP发送连接请求。ServerSocket实例则监听TCP连接请求,并为每个请求创建新的Socket实例。也就是说,服务器端要同时处理ServerSocket实例和Socket实例,而客户端只需要使用Socket实例。
我们从一个简单的客户端例子开始介绍。
2.2.1 TCP客户端
客户端向服务器发起连接请求后,就被动地等待服务器的响应。典型的TCP客户端要经过下面三步。
1.创建一个Socket实例:构造函数向指定的远程主机和端口建立一个TCP连接。
2.通过套接字的输入输出流(I/O streams)进行通信:一个Socket连接实例包括一个InputStream和一个OutputStream,它们的用法同于其他Java输入输出流(见2.2.3节)。
3.使用Socket类的close()方法关闭连接。
我们的第一个TCP应用程序叫TCPEchoClient.java,这是一个通过TCP协议与回馈服务器(echo server)进行通信的客户端。回馈服务器的功能只是简单地将收到的信息返回给客户端。在这个程序中,要回馈的字符串以命令行参数的型式传递给我们的客户端。很多系统都包含了用于进行调试和测试的回馈服务程序。你也许可以使用telnet程序来检测你的系统上是否运行了标准的回馈服务程序(如在命令行中输入“telnet server.example.com 7”),或者继续阅读本书,并运行下一节的服务器端示例程序。
TCPEchoClient.java
1.应用程序设置与参数解析:第0~17行
·转换回馈字符串:第15行
TCP套接字发送和接收字节序列信息。String类的getBytes()方法将返回代表该字符串的一个字节数组(见3.1节讨论的字符编码)。
·确定回馈服务器的端口号:第17行
默认端口号是7。如果我们给出了第三个参数,Integer.parseInt()方法就将第三个参数字符串转换成相应的整数,并作为端口号。
2.创建TCP套接字:第20行
Socket类的构造函数将创建一个套接字,并将其连接到由名字或IP地址指定的服务器,再将该套接字返回给程序。注意,底层的TCP协议只能处理IP地址,如果给出的是主机的名字,Socket类具体实现的时候会将其解析成相应的地址。若因某些原因连接失败,构造函数将抛出一个IOException异常。
3.获取套接字的输入输出流:第23~24行
每个Socket实例都关联了一个InputStream和一个OutputStream对象。就像使用其他流一样,我们通过将字节写入套接字的OutputStream来发送数据,并通过从InputStream读取信息来接收数据。
4.发送字符串到回馈服务器:第26行
OutputStream类的write()方法将指定的字节数组通过之前建立好的连接,传送到指定的服务器。
5.从回馈服务器接受回馈信息:第29~36行
既然已经知道要从回馈服务器接收的字节数,我们就能重复执行接收过程,直到接收了与发送的字节数相等的信息。这个特殊型式的read()方法需要3个参数:1)接收数据的字节数组,2)接收的第一个字节应该放入数组的位置,即字节偏移量,3)放入数组的最大字节数。read()方法在没有可读数据时会阻塞等待,直到有新的数据可读,然后读取指定的最大字节数,并返回实际放入数组的字节数(可能少于指定的最大字节数)。循环只是简单地将数据填入data字节数组,直到接收的字节数与发送的字节数一样。如果TCP连接被另一端关闭,read()方法返回-1。对于客户端来说,这表示服务器端提前关闭了套接字。
为什么不只用一个read方法呢?TCP协议并不能确定在read()和write()方法中所发送信息的界限,也就是说,虽然我们只用了一个write()方法来发送回馈字符串,回馈服务器也可能从多个块(chunk)中接受该信息。即使回馈字符串在服务器上存于一个块中,在返回的时候,也可能被TCP协议分割成多个部分。对于初学者来说,最常见的错误就是认为由一个write()方法发送的数据总是会由一个read()方法来接收。
6.打印回馈字符串:第38行
要打印服务器的响应信息,我们必须通过默认的字符编码将字节数组转换成一个字符串。
7.关闭套接字:第40行
当客户端接收到所有回馈数据后,将关闭套接字。
我们可以使用以下两种方法来与一个名叫server.example.com,IP地址为192.0.2.1的回馈服务器进行通信。命令行运行方式与结果如下:
%java TCPEchoClient server. example.com“Echo this!”
Received:Echo this!
%java TCPEchoClient 192. 0.2.1“Echo this!”
Received:Echo this!
在本书的网站上可以参考TCPEchoClientGUI.java示例程序,该程序为TCP回馈客户端实现了一个图形界面。
Socket:创建
前4个构造函数在创建了一个TCP套接字后,先连接到(connect)指定的远程地址和端口号,再将其返回给程序。前两个构造函数没有指定本地地址和端口号,因此将采用默认地址和可用的端口号。在有多个接口的主机上指定本地地址是有用的。指定的目的地址字符串参数可以使用与InetAddress构造函数的参数相同的形式。最后一个构造函数创建一个没有连接的套接字,在使用它进行通信之前,必须进行显式连接(通过connect()方法,见下文)。
Socket:操作
connect()方法将使指定的终端打开一个TCP连接。SocketAddress抽象类代表了套接字地址的一般形式,它的子类InetSocketAddress是针对TCP/IP套接字的特殊型式(见下文介绍)。与远程主机的通信是通过与套接字相关联的输入输出流实现的。可以使用get……Stream()方法来获取这些流。
close()方法关闭套接字及其关联的输入输出流,从而阻止对其的进一步操作。shutDownInput()方法关闭TCP流的输入端,任何没有读取的数据都将被舍弃,包括那些已经被套接字缓存的数据、正在传输的数据以及将要到达的数据。后续的任何从套接字读取数据的尝试都将抛出异常。shutDownOutput()方法在输出流上也产生类似的效果,但在具体实现中,已经写入套接字输出流的数据,将被尽量保证能发送到另一端。详情见4.5节。
注意:默认情况下,Socket是在TCP连接的基础上实现的,但是在Java中,你可以改变Socket的底层连接。由于本书是关于TCP/IP的,因此为了简便我们假设所有这些网络类的底层实现都与默认情况一致。
Socket:获取/检测属性
这些方法返回套接字的相应属性。实际上,本书中所有返回SocketAddress的方法返回的都是InetSocketAddress实例,而InetSocketAddress中封装了一个InetAddress和一个端口号。
Socket类实际上还有大量的其他相关属性,称为套接字选项(socket option)。这些属性对于编写基本应用程序是不必要的,因此我们推迟到第4.4节才对它们进行介绍。
InetSocketAddress:创建与访问
InetSocketAddress类为主机地址和端口号提供了一个不可变的组合。只接收端口号作为参数的构造函数将使用特殊的“任何”地址来创建实例,这点对于服务器端非常有用。接收字符串主机名的构造函数会尝试将其解析成相应的IP地址,而createUnresolved()静态方法允许在不对主机名进行解析情况下创建实例。如果在创建InetSocketAddress实例时没有对主机名进行解析,或解析失败,isUnresolved()方法将返回true。get……()系列方法提供了对指定属性的访问,getHostName()方法将返回InetSocketAddress内部InetAddress所关联的主机名。toString()方法重写了Object类的toString()方法,返回一个包含了主机名、数字型地址(如果已知)和端口号的字符串。其中,主机名与地址之间由‘/’(斜线)隔开,地址和端口号之间由‘:’(冒号)隔开。如果InetSocketAddress的主机名没有解析,则冒号前只有创建实例时的主机名字符串。