TCP套接字
Java为TCP协议提供了两个类:Socket类和ServerSocket类。一个Socket实例代表了TCP连接的一端。一个TCP连接(TCP connection)是一条抽象的双向信道,两端分别由IP地址和端口号确定。在开始通信之前,要建立一个TCP连接,这需要先由客户端TCP向服务器端TCP发送连接请求。ServerSocket实例则监听TCP连接请求,并为每个请求创建新的Socket实例。也就是说,服务器端要同时处理ServerSocket实例和Socket实例,而客户端只需要使用Socket实例。
我们从一个简单的客户端例子开始介绍。
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
0 import java.net.Socket;
1 import java.net.SocketException;
2 import java.io.IOException;
3 import java.io.InputStream;
4 import java.io.OutputStream;
5
6 public class TCPEchoClient {
7
8 public static void main(String[] args) throws
IOException {
9
10 if ((args.length < 2) || (args.length > 3)) // Test
for correct # of args
11 throw new IllegalArgumentException("Parameter(s):
<Server> <Word> [<Port>]");
12
13 String server = args[0]; // Server name or IP address
14 // Convert argument String to bytes using the default
character encoding
15 byte[] data = args[1].getBytes();
16
17 int servPort = (args.length == 3) ?
Integer.parseInt(args[2]) : 7;
18
19 // Create socket that is connected to server on
specified port
20 Socket socket = new Socket(server, servPort);
21 System.out.println("Connected to server...sending
echo string");
22
23 InputStream in = socket.getInputStream();
24 OutputStream out = socket.getOutputStream();
25
26 out.write(data); // Send the encoded string to the
server
27
28 // Receive the same string back from the server
29 int totalBytesRcvd = 0; // Total bytes received so
far
30 int bytesRcvd; // Bytes received in last read
31 while (totalBytesRcvd < data.length) {
32 if ((bytesRcvd = in.read(data, totalBytesRcvd,
33 data.length - totalBytesRcvd)) == -1)
34 throw new SocketException("Connection closed
prematurely");
35 totalBytesRcvd += bytesRcvd;
36 } // data array is full
37
38 System.out.println("Received: " + new String(data));
39
40 socket.close(); // Close the socket and its streams
41 }
42 }
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()方法来发送回馈字符串,回馈服务器也可能从多个块(chunks)中接受该信息。即使回馈字符串在服务器上存于一个块中,在返回的时候,也可能被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: 创建
Socket(InetAddress remoteAddr, int remotePort)
Socket(String remoteHost, int remotePort)
Socket(InetAddress remoteAddr, int remotePort,
InetAddress localAddr, int localPort)
Socket(String remoteHost, int remotePort,
InetAddress localAddr, int localPort)
Socket()
前四个构造函数在创建了一个TCP套接字后,先连接到(connect)指定的远程地址和端口号,再将其返回给程序。前两个构造函数没有指定本地地址和端口号,因此将采用默认地址和可用的端口号。在有多个接口的主机上指定本地地址是有用的。指定的目的地址字符串参数可以使用与InetAddress构造函数的参数相同的型式。最后一个构造函数创建一个没有连接的套接字,在使用它进行通信之前,必须进行显式连接(通过connect()方法,见下文)。
Socket: 操作
void connect(SocketAddress destination)
void connect(SocketAddress destination, int timeout)
InputStream getInputStream()
OutputStream getOutputStream()
void close()
void shutdownInput()
void shutdownOutput()
connect()方法将使指定的终端打开一个TCP连接。SocketAddress抽象类代表了套接字地址的一般型式,它的子类InetSocketAddress是针对TCP/IP套接字的特殊型式(见下文介
绍)。与远程主机的通信是通过与套接字相关联的输入输出流实现的。可以使用get...Stream()方法来获取这些流。
close()方法关闭套接字及其关联的输入输出流,从而阻止对其的进一步操作。
shutDownInput()方法关闭TCP流的输入端,任何没有读取的数据都将被舍弃,包括那些已经被套接字缓存的数据、正在传输的数据以及将要到达的数据。后续的任何从套接字读取数据的尝试都将抛出异常。shutDownOutput()方法在输出流上也产生类似的效果,但在具体实现中,已经写入套接字输出流的数据,将被尽量保证能发送到另一端。
注意:默认情况下,Socket是在TCP连接的基础上实现的,但是在Java中,你可以改变Socket的底层连接。由于本书是关于TCP/IP的,因此为了简便我们假设所有这些网络类的底层实现都与默认情况一致。
Socket: 获取/检测属性
InetAddress getInetAddress()
int getPort()
InetAddress getLocalAddress()
int getLocalPort()
SocketAddress getRemoteSocketAddress()
SocketAddress getLocalSocketAddress()
这些方法返回套接字的相应属性。实际上,本书中所有返回SocketAddress的方法返回的都是InetSocketAddress实例,而InetSocketAddress中封装了一个InetAddress和一个端口
号。
Socket类实际上还有大量的其他相关属性,称为套接字选项(socket options)。这些属性对于编写基本应用程序是不必要的,因此我们推迟到第4.4节才对它们进行介绍。
InetSocketAddress: 创建与访问
InetSocketAddress(InetAddress addr, int port)
InetSocketAddress(int port)
InetSocketAddress(String hostname, int port)
static InetSocketAddress createUnresolved(String host,
int port)
boolean isUnresolved()
InetAddress getAddress()
int getPort()
String getHostName()
String toString()
InetSocketAddress类为主机地址和端口号提供了一个不可变的组合。只接收端口号作为参数的构造函数将使用特殊的"任何"地址来创建实例,这点对于服务器端非常有用。接收字符串主机名的构造函数会尝试将其解析成相应的IP地址,而createUnresolved()静态方法允许在不对主机名进行解析情况下创建实例,。如果在创建 InetSocketAddress 实例时没有对主机名进行解析,或解析失败,isUnresolved()方法将返回true。get...()系列方法提供了对指定属性的访问,getHostName()方法将返回InetSocketAddress内InetAddress所关联的主机名。toString()方法重写了Object类的toString()方法,返回一个包含了主机名、数字型地址(如果已知)和端口号的字符串。其中,主机名与地址之间由'/'(斜线)隔开,地址和端口号之间由':'(冒号)隔开。如果InetSocketAddress的主机名没有解析,则冒号前只有创建实例时的主机名字符串。
2 TCP服务器端
现在我们将注意力转向如何创建一个服务器端。服务器端的工作是建立一个通信终端,并被动地等待客户端的连接。典型的TCP服务器有如下两步工作:
1. 创建一个ServerSocket实例并指定本地端口。此套接字的功能是侦听该指定端口收到的连接。
2. 重复执行:
a.调用ServerSocket的accept()方法以获取下一个客户端连接。基于新建立的客户端连接,创建一个Socket实例,并由accept()方法返回。
b. 使用所返回的Socket实例的InputStream和OutputStream与客户端进行通信。
c.通信完成后,使用Socket类的close()方法关闭该客户端套接字连接。
下面的例子,TCPEchoServer.java,为我们前面的客户端程序实现了一个回馈服务器。这个服务器程序非常简单,它将一直运行,反复接受连接请求,接收并返回字节信息。直到客户端关闭了连接,它才关闭客户端套接字。
TCPEchoServer.java
0 import java.net.*; // for Socket, ServerSocket, and
InetAddress
1 import java.io.*; // for IOException and
Input/OutputStream
2
3 public class TCPEchoServer {
4
5 private static final int BUFSIZE = 32; // Size of receive
buffer
6
7 public static void main(String[] args) throws
IOException {
8
9 if (args.length != 1) // Test for correct # of args
10 throw new IllegalArgumentException("Parameter(s):
<Port>");
11
12 int servPort = Integer.parseInt(args[0]);
13
14 // Create a server socket to accept client connection
requests
15 ServerSocket servSock = new ServerSocket(servPort);
16
17 int recvMsgSize; // Size of received message
18 byte[] receiveBuf = new byte[BUFSIZE]; // Receive
buffer
19
20 while (true) { // Run forever, accepting and servicing
connections
21 Socket clntSock = servSock.accept(); // Get client
connection
22
23 SocketAddress clientAddress =
clntSock.getRemoteSocketAddress();
24 System.out.println("Handling client at " +
clientAddress);
25
26 InputStream in = clntSock.getInputStream();
27 OutputStream out = clntSock.getOutputStream();
28
29 // Receive until client closes connection, indicated
by -1 return
30 while ((recvMsgSize = in.read(receiveBuf)) != -1)
{
31 out.write(receiveBuf, 0, recvMsgSize);
32 }
33 clntSock.close(); // Close the socket. We are done
with this client!
34 }
35 /* NOT REACHED */
36 }
37 }
TCPEchoServer.java
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()方法以"/_address_:_port_"的形式打印出这些信息。(主机名部分为空,因为该实例只根据地址信息创建。)
获取套接字的输入输出流:第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: 创建
ServerSocket(int localPort)
ServerSocket(int localPort, int queueLimit)
ServerSocket(int localPort, int queueLimit, InetAddress
localAddr)
ServerSocket()
一个TCP终端必须与特定的端口号关联,以使客户端能够向该端口号发送连接请求。上面前三个构造函数创建一个TCP端口,此端口与特定的本地端口相关联且已准备好接受(accept)传入的连接请求。端口号的有效范围是0-65535。(如果端口号被设为0,将选择任意没有使用的端口号)连接队列的大小以及本地地址也可以选择设置。需要注意的是,最大队列长度不是一个严格的限制,也不能用来控制客户端的总数。如果指定了本地地址,该地址就必须是主机的网络接口之一;如果没有指定,套接字将接受指向主机任何IP地址的连接。这将对有多个接口而服务器端只接受其中一个接口连接的主机非常有用。
第四个构造函数能创建一个没有关联任何本地端口的ServerSocket实例。在使用该实例前,必须为其绑定(见下文中的bind()方法)一个端口号。
ServerSocket: 操作
void bind(int port)
void bind(int port, int queuelimit)
Socket accept()
void close()
bind()方法为套接字关联一个本地端口。每个ServerSocket实例只能与唯一一个端口相关联。如果该实例已经关联了一个端口,或所指定的端口已经被占用,则将抛出IOException异常。
accept()方法为下一个传入的连接请求创建Socket实例,并将已成功连接的Socket实例返回给服务器端套接字。如果没有连接请求等待,accept()方法将阻塞等待,直到有新的连接请求到来或超时。
close()方法关闭套接字。调用该方法后,服务器将拒绝接受传入该套接字的客户端连接请求。
ServerSocket: 获取属性
InetAddress getInetAddress()
SocketAddress getLocalSocketAddress()
int getLocalPort()
以上方法将返回服务器端套接字的本地地址和端口号。注意,与Socket 类不同的是,ServerSocket没有相关联的I/O流。然而,它有另外一些称为选项(options)的属性,并能通过多种方法对选项进行控制。
相关下载:
Java_TCPIP_Socket编程(doc)
http://download.csdn.net/detail/undoner/4940239
文献来源:
UNDONER(小杰博客) :http://blog.csdn.net/undoner
LSOFT.CN(琅软中国) :http://www.lsoft.cn