admin 管理员组文章数量: 1184232
2023年12月17日发(作者:数据库除了增删改查还有什么)
java socket通信
1. 1简介
2. 2基本套接字
1. TCP套接字
1. TCP客户端
2. TCP服务器端
2. UDP套接字
1. UDP客户端
2. UDP服务器端
3. 3发送和接收数据
1. 信息编码
1. 基本整型
2. 字符串和文本
3. 成帧与解析
4. 构建和解析协议消息
1. 消息类VoteMsg展示了每条消息中的基本信息
2. 编码和解码类接口VoteMsgCoder
3. 基于文本的编码解码类VoteMsgTextCoder
4. 基于二进制的编码解码类VoteMsgBinCoder
5. 服务器中记录投票过程的服务类VoteService
6. TCP投票客户端类VoteClientTCP
7. TCP投票服务器端类VoteServerTCP
8. UDP投票客户端类VoteClientUDP
9. UDP投票服务器端类VoteServerUDP
4. 4多任务处理
1. 一客户一线程
2. 线程池
3. 利用JDK 提供的线程池javautilconcurrent包中来实现并行服务器
4. 阻塞和超时
5. 多接收者
1. 广播
2. 多播
6. Keep-Alive机制
7. 发送和接收缓存区的大小
8. 消除缓冲延迟
9. 关闭连接
5. 4NIO
1. Buffer详解
2. 流TCP信道详解
3. Selector详解
4. 数据报UDP信道
1:简介
Java语言从一开始就是为了让人们使用互联网而设计的,它为实现程序的相互通信提供了许多有用的抽象应用程序接口(API, Application Programming Interface),这类应用程序接口被称为套接字(sockets)。
信息(information)是指由程序创建和解释的字节序列。在计算机网络环境中,这些字节序列被称为分组报文(packets)。一组报文包括了网络用来完成工作的控制信息,有时还包括一些用户数据。用于定位分组报文目的地址的信息就是一个例子。路由器正是利用了这些控制信息来实现对每个报文的转发。
协议(protocol)相当于是相互通信的程序间达成的一种约定,它规定了分组报文的交换方式和它们包含的意义。一组协议规定了分组报文的结构(例如报文中的哪一部分表明了其目的地址)以及怎样对报文中所包含的信息进行解析。设计一组协议,通常是为了在一定约束条件下解决某一特定的问题。比如,超文本传输协议(HTTP,HyperText Transfer
Protocol)是为了解决在服务器间传递超文本对象的问题,这些超文本对象在服务器中创建和存储,并由Web浏览器进行可视化,以使其对用户有用。即时消息协议是为了使两个或更多用户间能够交换简短的文本信息。
Application:应用程序;Socket:套接字;Host:主机;Channel:通信信道;Ethernet:以太网;Router:路由器;Network Layer:网络层;Transport Layer:传输层。
IP协议提供了一种数据报服务:每组分组报文都由网络独立处理和分发,就像信件或包裹通过邮政系统发送一样。为了实现这个功能,每个IP报文必须包含一个保存其目的地址(address)的字段,就像你所投递的每份包裹都写明了收件人地址。(我们随即会对地址进行更详细的说明。)尽管绝大部分递送公司会保证将包裹送达,但IP协议只是一个"尽力而为"(best-effort)的协议:它试图分发每一个分组报文,但在网络传输过程中,偶
尔也会发生丢失报文,使报文顺序被打乱,或重复发送报文的情况。
IP协议层之上称为传输层(transport layer)。它提供了两种可选择的协议:TCP协议和UDP协议。这两种协议都建立在IP层所提供的服务基础上,但根据应用程序协议(application protocols)的不同需求,它们使用了不同的方法来实现不同方式的传输。TCP协议和UDP协议有一个共同的功能,即寻址。回顾一下,IP协议只是将分组报文分发到了不同的主机,很明显,还需要更细粒度的寻址将报文发送到主机中指定的应用程序,因为同一主机上可能有多个应用程序在使用网络。TCP协议和UDP协议使用的地址叫做端口号(port numbers),都是用来区分同一主机中的不同应用程序。TCP协议和UDP协议也称为端到端传输协议(end-to-end transport protocols),因为它们将数据从一个应用程序传输到另一个应用程序,而IP协议只是将数据从一个主机传输到另一主机。
TCP协议能够检测和恢复IP层提供的主机到主机的信道中可能发生的报文丢失、重复及其他错误。TCP协议提供了一个可信赖的字节流(reliable byte-stream)信道,这样应用程序就不需要再处理上述的问题。TCP协议是一种面向连接(connection-oriented)的协议:在使用它进行通信之前,两个应用程序之间首先要建立一个TCP连接,这涉及到相互通信的两台电脑的TCP部件间完成的握手消息(handshake messages)的交换。使用TCP协议在很多方面都与文件的输入输出(I/O, Input/Output)相似。实际上,由一个程序写入的文件再由另一个程序读取就是一个TCP连接的适当模型。另一方面,UDP协议并不尝试对IP层产生的错误进行修复,它仅仅简单地扩展了IP协议"尽力而为"的数据报服务,使它能够在应用程序之间工作,而不是在主机之间工作。因此,使用了UDP协议的应用程序必须为处理报文丢失、顺序混乱等问题做好准备。
在TCP/IP协议中,有两部分信息用来定位一个指定的程序:互联网地址(Internet
address)和端口号(port number)。其中互联网地址由IP协议使用,而附加的端口地址信息由传输协议(TCP或IP协议)对其进行解析。互联网地址由二进制的数字组成,有两种型式,分别对应了两个版本的标准互联网协议。现在最常用的版本是版本4,即IPv4,另一个版本是刚开始开发的版本6,即IPv。IPv4的地址长32位,只能区分大约40亿个独立地址,对于如今的互联网来说,这是不够大的。(也许看起来很多,但由于地址的分配方式的原因,有很多都被浪费了)出于这个原因引入了IPv6,它的地址有128位长。
一台主机,只要它连接到网络,一个互联网地址就能定位这条主机。但是反过来,一台主机并不对应一个互联网地址。因为每台主机可以有多个接口,每个接口又可以有多个地址。(实际上一个接口可以同时拥有IPv4地址和IPv6地址)。端口号是一组16位的无符号二进制数,每个端口号的范围是1到65535。(0被保留)。每个版本的IP协议都定义了一些特殊用途的地址。其中值得注意的一个是回环地址(loopback address),该地
址总是被分配个一个特殊的回环接口(loopback interface)。回环接口是一种虚拟设备,它的功能只是简单地将发送给它的报文直接回发给发送者。IPv4的回环地址是127.0.0.1[,IPv6的回环地址是0:0:0:0:0:0:0:1。
IPv4地址中的另一种特殊用途的保留地址包括那些"私有用途"的地址。它们包括IPv4中所有以10或192.168开头的地址,以及第一个数是172,第二个数在16到31的地址。(在IPv6中没有相应的这类地址)这类地址最初是为了在私有网络中使用而设计的,不属于公共互联网的一部分。现在这类地址通常被用在家庭或小型办公室中,这些地方通过NAT(Network Address Translation,网络地址转换)设备连接到互联网。NAT设备的功能就像一个路由器,转发分组报文时将转换(重写)报文中的地址和端口。更准确地说,它将一个接口中报文的私有地址端口对(private address, port pairs)映射成另一个接口中的公有地址端口对(public address, port pairs)。这就使一小组主机(如家庭网络)能够有效地共享同一个IP地址。重要的是这些内部地址不能从公共互联网访问。
多播(multicast)地址。普通的IP地址(有时也称为"单播"地址)只与唯一一个目的地址相关联,而多播地址可能与任意数量的目的地址关联。IPv4中的多播地址在点分格式中,第一个数字在224到239之间。IPv6中,多播地址由FF开始。
习惯于通过名字来指代一个主机,例如:。然而,互联网协议只能处理二进制的网络地址,而不是主机名。首先应该明确的是,使用主机名而不使用地址是出于方便性的考虑,这与TCP/IP提供的基本服务是相互独立的。你也可以不使用名字来编写和使用TCP/IP应用程序。当使用名字来定位一个通信终端时,系统将做一些额外的工作把名字解析成地址。有两个原因证明这额外的步骤是值得的:第一,相对于点分形式(或IPv6中的十六进制数字串),人们更容易记住名字;第二,名字提供了一个间接层,使IP地址的变化对用户不可见。如网络服务器的地址就改变过。由于我们通常都使用网络服务器的名字,而且地址的改变很快就被反应到映射主机名和网络地址的服务上,如从之前的地址208.164.121.48对应到了现在的地址,这种变化对通过名字访问该网络服务器的程序是透明的。名字解析服务可以从各种各样的信息源获取信息。两个主要的信息源是域名系统(DNS,Domain Name System)和本地配置数据库。DNS是一种分布式数据库。DNS协议允许连接到互联网的主机通过TCP或UDP协议从DNS数据库中获取信息。本地配置数据库通常是一种与具体操作系统相关的机制,用来实现本地名称与互联网地址的映射。
客户端(client)和服务器(server)这两个术语代表了两种角色:客户端是通信的发起者,而服务器程序则被动等待客户端发起通信,并对其作出响应。客户端与服务器组成了应用程序(application)。服务器具有一定的特殊能力,如提供数据库服务,并使任何客
户端能够与之通信。一个程序是作为客户端还是服务器,决定了它在与其对等端(peer)建立通信时使用的套接字API的形式(客户端的对等端是服务器,反之亦然)。更进一步来说,客户端与服务器端的区别非常重要,因为客户端首先需要知道服务器的地址和端口号,反之则不需要。如果有必要,服务器可以使用套接字API,从收到的第一个客户端通信消息中获取其地址信息。这与打电话非常相似:被呼叫者不需要知道拨电话者的电话号码。就像打电话一样,只要通信连接建立成功,服务器和客户端之间就没有区别了。服务器可以使用任何端口号,但客户端必须能够获知这些端口号。在互联网上,一些常用的端口号被约定赋给了某些应用程序。
Socket(套接字)是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个socket允许应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向socket写入的信息能够被另一台计算机上的另一个应用程序读取,反之亦然。
Applications:应用程序;TCP sockets:TCP套接字;TCP ports:TCP端口;Socket
References:套接字引用;UDP sockets:UDP套接字;Sockets bound to ports:套接字绑定到端口;UDP ports:UDP端口。
不同类型的socket与不同类型的底层协议族以及同一协议族中的不同协议栈相关联。现在TCP/IP协议族中的主要socket类型为流套接字(sockets sockets)和数据报套接字(datagram sockets)。流套接字将TCP作为其端对端协议(底层使用IP协议),提供了一个可信赖的字节流服务。一个TCP/IP流套接字代表了TCP连接的一端。数据报套接字使用UDP协议(底层同样使用IP协议),提供了一个"尽力而为"(best-effort)的数据报服务,应用程序可以通过它发送最长65500字节的个人信息。当然,其他协议族也支持
流套接字和数据报套接字,本文只对TCP流套接字和UDP数据报套接字进行讨论。一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP或UDP协议)以及一个端口号唯一确定。主机中的多个程序可以同时访问同一个套接字。在实际应用中,访问相同套接字的不同程序通常都属于同一个应用(例如,Web服务程序的多个拷贝),但从理论上讲,它们是可以属于不同应用的。
2:基本套接字
一个客户端要发起一次通信,首先必须知道运行服务器端程序的主机的IP地址。然后由网络的基础结构利用目标地址(destination address),将客户端发送的信息传递到正确的主机上。在Java中,地址可以由一个字符串来定义,这个字符串可以是数字型的地址(不同版本的IP地址有不同的型式,如192.0.2.27是一个IPv4地址,
fe20:12a0::0abc:1234是一个IPv6地址),也可以是主机名(如)。主机名必须能够被解析(resolved)成数字型地址才能用来进行通信。
NetworkInterface:NetworkInterface类提供了访问主机所有接口的信息的功能。(IP地址实际上是分配给了主机与网络之间的连接,而不是主机本身)
InetAddress:网络接口,代表了一个网络目标地址,包括主机名和数字类型的地址信息。该类有两个子类,Inet4Address和Inet6Address,分别对应了目前IP地址的两个版本。InetAddress实例是不可变的,一旦创建,每个实例就始终指向同一个地址。
SocketAddress:抽象类,代表了套接字地址的一般型式,它的子类InetSocketAddress是针对TCP/IP套接字的特殊型式,封装了一个InetAddress和一个端口号。InetSocketAddress类为主机地址和端口号提供了一个不可变的组合。只接收端口号作为参数的构造函数将使用特殊的"任何"地址来创建实例,这点对于服务器端非常有用。接收字符串主机名的构造函数会尝试将其解析成相应的IP地址。
Socket和ServerSocket:Java为TCP协议提供了两个类:Socket类和ServerSocket类。一个Socket实例代表了TCP连接的一端。一个TCP连接(TCP connection)是一条抽象的双向信道,两端分别由IP地址和端口号确定。在开始通信之前,要建立一个TCP连接,这需要先由客户端TCP向服务器端TCP发送连接请求。ServerSocket实例则监听TCP连接请求,并为每个请求创建新的Socket实例。也就是说,服务器端要同时处理ServerSocket实例和Socket实例,而客户端只需要使用Socket实例。
DatagramPacket:Java程序员通过DatagramPacket 类和 DatagramSocket类来使用UDP套接字。客户端和服务器端都使用DatagramSockets来发送数据,使用DatagramPackets来接收数据。
DatagramPacket:UDP终端交换的是一种称为数据报文的自包含(self-contained)信息。这种信息在Java中表示为DatagramPacket类的实例,发送信息时,Java程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,Java程序首先创建一个DatagramPacket实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。
[java] view plaincopy
1. import ation;
2. import .*;
3.
4. public class InetAddressExample {
5.
6. public static void main(String[] args) {
7.
8. // Get the network interfaces and associated addresses for this host
9. try {
10. Enumeration
11. if (interfaceList == null) {
12. n("--No interfaces found--");
13. } else {
14. while (eElements()) {
15. NetworkInterface iface = ement();
16. n("Interface " + e() + ":");
17. Enumeration
18. if (!eElements()) {
19. n("t(No addresses for this interface)");
20. }
21. while (eElements()) {
22. InetAddress address = ement();
23. ("tAddress "+ ((address instanceof
Inet4Address ? "(v4)": (address instanceof Inet6Address ? "(v6)": "(?)"))));
24. n(": " + tAddress());
25. }
26. }
27. }
28. } catch (SocketException se) {
29. n("Error getting network interfaces:"+ sage());
30. }
31.
32. // Get name(s)/address(es) of hosts given on command line
33. for (String host : args) {
34. try {
35. n(host + ":");
36. InetAddress[] addressList = ByName(host);
37. for (InetAddress address : addressList) {
38. n("t" + tName() + "/"+ tAddress());
39. }
40. } catch (UnknownHostException e) {
41. n("tUnable to find address for " + host);
42. }
43. }
44. }
45. }
运行结果:
% java InetAddressExample 129.35.69.7
Interface lo:
Address (v4): 127.0.0.1
Address (v6): 0:0:0:0:0:0:0:1
Address (v6): fe80:0:0:0:0:0:0:1%1
Interface eth0:
Address (v4): 192.168.159.1
Address (v6): fe80:0:0:0:250:56ff:fec0:8%4
:
/129.35.69.7
:
Unable to find address for
129.35.69.7:
129.35.69.7/129.35.69.7
地址解析器在放弃对一个主机名的解析之前,会到多个不同的地方查找该主机名。如果由于某些原因使名字服务失效(例如由于程序所运行的机器并没有连接到所有的网络),试图通过名字来定位一个主机就可能失败。而且这还将耗费大量的时间,因为系统将尝试各种不同的方法来将主机名解析成IP地址,因此最好能直接使用点分形式的IP地址来访问一个主机
InetAddress: 创建和访问
static InetAddress[ ] getAllByName(String host)
static InetAddress getByName(String host)
static InetAddress getLocalHost()
byte[] getAddress()
InetAddress: 字符串表示
String toString()
String getHostAddress()
String getHostName()
String getCanonicalHostName()
InetAddress: 检测属性
boolean isAnyLocalAddress()
boolean isLinkLocalAddress()
boolean isLoopbackAddress()
boolean isMulticastAddress()
boolean isMCGlobal()
boolean isMCLinkLocal()
boolean isMCNodeLocal()
boolean isMCOrgLocal()
boolean isMCSiteLocal()
boolean isReachable(int timeout)
boolean isReachable(NetworkInterface netif, int ttl, int timeout)
最后两个方法检查是否真能与InetAddress地址确定的主机进行数据报文交换。注意,与其他句法检查方法不一样的是,这些方法引起网络系统执行某些动作,即发送数据报文。系统不断尝试发送数据报文,直到指定的时间(以毫秒为单位)用完才结束。后面这种形式更详细:它明确指出数据报文必须经过指定的网络接口(NetworkInterface),并检查其是否能在指定的生命周期(time-to-live,TTL)内联系上目的地址。TTL限制了一个数据报文在网络上能够传输的距离。后面两个方法的有效性通常还受到安全管理配置方面的限制。
NetworkInterface: 创建,获取信息
static EnumerationNetworkInterface
getNetworkInterfaces()
static NetworkInterface getByInetAddress(InetAddress addr)
static NetworkInterface getByName(String name)
EnumerationInetAddress getInetAddresses()
String getName()
String getDisplayName()
上面第一个方法非常有用,使用它可以很容易获取到运行程序的主机的IP地址:通过getNetworkInterfaces()方法可以获取一个接口列表,再使用实例的getInetAddresses()方法就可以获取每个接口的所有地址。注意:这个列表包含了主机的所有接口,包括不能够向网络中的其他主机发送或接收消息的虚拟回环接口。同样,列表中可能还包括外部不可达的本地链接地址。由于这些列表都是无序的,所以你不能简单地认为,列表中第一个接口的第一个地址一定能够通过互联网访问,而是要通过前面提到的InetAddress类的属性检查方法,来判断一个地址不是回环地址,不是本地链接地址等等。getName()方法返回一个接口(interface)的名字(不是主机名)。这个名字由字母字符串加上一个数字组成,如eth0。在很多系统中,回环地址的名字都是lo0。
TCP套接字
TCP客户端
客户端向服务器发起连接请求后,就被动地等待服务器的响应。典型的TCP客户端要经过下面三步:
1.创建一个Socket实例:构造器向指定的远程主机和端口建立一个TCP连接。
2. 通过套接字的输入输出流(I/O streams)进行通信:一个Socket连接实例包括一个InputStream和一个OutputStream,它们的用法同于其他Java输入输出流。
3. 使用Socket类的close()方法关闭连接。
(这是一个通过TCP协议与回馈服务器(echo server)进行通信的客户端)
[java] view plaincopy
1. import ;
2. import Exception;
3. import ption;
4. import tream;
5. import Stream;
6.
7. public class TCPEchoClient {
8. public static void main(String[] args) throws IOException {
9. if (( < 2) || ( > 3)) // Test for correct # of
args
10. throw new IllegalArgumentException("Parameter(s):
11. String server = args[0]; // Server name or IP address
12. // Convert argument String to bytes using the default character encoding
13. byte[] data = args[1].getBytes();
14. int servPort = ( == 3) ? nt(args[2]) : 7;
15. // Create socket that is connected to server on specified port
16. Socket socket = new Socket(server, servPort);
17. n("Connected sending echo string");
18. InputStream in = utStream();
19. OutputStream out = putStream();
20. (data); // Send the encoded string to the server
21. // Receive the same string back from the server
22. int totalBytesRcvd = 0; // Total bytes received so far
23. int bytesRcvd; // Bytes received in last read
24. while (totalBytesRcvd < ) {
25. if ((bytesRcvd = (data, totalBytesRcvd, - totalBytesRcvd)) == -1)
26. throw new SocketException("Connection closed prematurely");
27. totalBytesRcvd += bytesRcvd;
28. } // data array is full
29. n("Received: " + new String(data));
30. (); // Close the socket and its streams
31. }
32. }
为什么不只用一个read方法呢?TCP协议并不能确定在read()和write()方法中所发送信息的界限,也就是说,虽然我们只用了一个write()方法来发送回馈字符串,回馈服务器也可能从多个块(chunks)中接受该信息。即使回馈字符串在服务器上存于一个块中,在返回的时候,也可能被TCP协议分割成多个部分。对于初学者来说,最常见的错误就是认为由一个write()方法发送的数据总是会由一个read()方法来接收。
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()
前两个构造函数没有指定本地地址和端口号,因此将采用默认地址和可用的端口号。在有多个接口的主机上指定本地地址是有用的。指定的目的地址字符串参数可以使用与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套接字的特殊型式。与远程主机的通信是通过与套接字相关联的输入输出流实现的。可以使用Stream()方法来获取这些流。close()方法关闭套接字及其关联的输入输出流,从而阻止对其的进一步操作。shutDownInput()方法关闭TCP流的输入端,任何没有读取的数据都将被舍弃,包括那些已经被套接字缓存的数据、正在传输的数据以及将要到达的数据。后续的任何从套接
字读取数据的尝试都将抛出异常。shutDownOutput()方法在输出流上也产生类似的效果,但在具体实现中,已经写入套接字输出流的数据,将被尽量保证能发送到另一端。注意:默认情况下,Socket是在TCP连接的基础上实现的,但是在Java中,你可以改变Socket的底层连接。
Socket: 获取/检测属性
InetAddress getInetAddress()
int getPort()
InetAddress getLocalAddress()
int getLocalPort()
SocketAddress getRemoteSocketAddress()
SocketAddress getLocalSocketAddress()
Socket类实际上还有大量的其他相关属性,称为套接字选项(socket options)。这些属性对于编写基本应用程序是不必要的
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()
createUnresolved()静态方法允许在不对主机名进行解析情况下创建实例
TCP服务器端
服务器端的工作是建立一个通信终端,并被动地等待客户端的连接。典型的TCP服务器有如下两步工作:
1. 创建一个ServerSocket实例并指定本地端口。此套接字的功能是侦听该指定端口收到的连接。
2. 重复执行:
a. 调用ServerSocket的accept()方法以获取下一个客户端连接。基于新建立的客户端连接,创建一个Socket实例,并由accept()方法返回。
b. 使用所返回的Socket实例的InputStream和OutputStream与客户端进行通信。
c. 通信完成后,使用Socket类的close()方法关闭该客户端套接字连接。
(为我们前面的客户端程序实现了一个回馈服务器。这个服务器程序非常简单,它将一直运行,反复接受连接请求,接收并返回字节信息。直到客户端关闭了连接,它才关闭客户端套接字。)
[java] view plaincopy
1. import .*; // for Socket, ServerSocket, and InetAddress
2. import .*; // for IOException and Input/OutputStream
3.
4. public class TCPEchoServer {
5.
6. private static final int BUFSIZE = 32; // Size of receive buffer
7.
8. public static void main(String[] args) throws IOException {
9. if ( != 1) // Test for correct # of args
10. throw new IllegalArgumentException("Parameter(s):
11. int servPort = nt(args[0]);
12. // Create a server socket to accept client connection requests
13. ServerSocket servSock = new ServerSocket(servPort);
14. int recvMsgSize; // Size of received message
15. byte[] receiveBuf = new byte[BUFSIZE]; // Receive buffer
16. while (true) { // Run forever, accepting and servicing connections
17. Socket clntSock = (); // Get client connection
18. SocketAddress clientAddress = oteSocketAddress();
19. n("Handling client at " + clientAddress);
20. InputStream in = utStream();
21. OutputStream out = putStream();
22. // Receive until client closes connection, indicated by -1 return
23. while ((recvMsgSize = (receiveBuf)) != -1) {
24. (receiveBuf, 0, recvMsgSize);
25. }
26. (); // Close the socket. We are done with this client!
27. }
28. /* NOT REACHED */
29. }
30. }
ServerSocket: 创建
ServerSocket(int localPort)
ServerSocket(int localPort, int queueLimit)
ServerSocket(int localPort, int queueLimit, InetAddress localAddr)
ServerSocket()
如果指定了本地地址,该地址就必须是主机的网络接口之一;如果没有指定,套接字将接受指向主机任何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()
如果在一个TCP套接字关联的输出流上进行操作,当大量的数据已发送,而连接的另一端所关联的输入流最近没有调用read()方法时,OutputStream中的方法可能会阻塞。如果不作特殊处理,这可能会产生一些不想得到的后果。在一个TCP套接字关联的输入流上没有数据可读,而又没有检测到流结束标记时,所有的read()方法都将阻塞等待,直到至少有
一个字节可读。在没有数据可读,同时又检测到流结束标记时,InputStream中的方法都将返回-1。
UDP套接字
UDP协议提供了一种不同于TCP协议的端到端服务。实际上UDP协议只实现两个功能:1)在IP协议的基础上添加了另一层地址(端口),2)对数据传输过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。由于其简单性,UDP套接字具有一些与我们之前所看到的TCP套接字不同的特征。例如,UDP套接字在使用前不需要进行连接。TCP协议与电话通信相似,而UDP协议则与邮件通信相似:你寄包裹或信件时不需要进行"连接",但是你得为每个包裹和信件指定目的地址。类似的,每条信息(即数据报文,datagram)负载了自己的地址信息,并与其他信息相互独立。在接收信息时,UDP套接字扮演的角色就像是一个信箱,从不同地址发送来的信件和包裹都可以放到里面。一旦被创建,UDP套接字就可以用来连续地向不同的地址发送信息,或从任何地址接收信息。UDP套接字与TCP套接字的另一个不同点在于他们对信息边界的处理方式不同:UDP套接字将保留边界信息。这个特性使应用程序在接受信息时,从某些方面来说比使用TCP套接字更简单。最后一个不同点是,UDP协议所提供的端到端传输服务是尽力而为(best-effort)的,即UDP套接字将尽可能地传送信息,但并不保证信息一定能成功到达目的地址,而且信息到达的顺序与其发送顺序不一定一致(就像通过邮政部门寄信一样)。因此,使用了UDP套接字的程序必须准备好处理信息的丢失和重排。
既然UDP协议为程序带来了这个额外的负担,为什么还会使用它而不使用TCP协议呢?原因之一是效率:如果应用程序只交换非常少量的数据,例如从客户端到服务器端的简单请求消息,或一个反方向的响应消息,TCP连接的建立阶段就至少要传输其两倍的信息量(还有两倍的往返延迟时间)。另一个原因是灵活性:如果除可靠的字节流服务外,还有其他的需求,UDP协议则提供了一个最小开销的平台来满足任何需求的实现。
与TCP协议发送和接收字节流不同,UDP终端交换的是一种称为数据报文的自包含(self-contained)信息。这种信息在Java中表示为DatagramPacket类的实例。发送信息时,Java程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,Java程序首先创建一个DatagramPacket实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。除传输的信息本身外,每个DatagramPacket实例中还附加了地址和端口信息,其具体含义取决于该数据报文是被发送还是被接收。若是要发送的数据报文,
DatagramPacket实例中的地址则指明了目的地址和端口号,若是接收到的数据报文,
DatagramPacket实例中的地址则指明了所收信息的源地址。
DatagramPacket: 创建
DatagramPacket(byte[ ] data, int length)
DatagramPacket(byte[ ] data, int offset, int length)
DatagramPacket(byte[ ] data, int length, InetAddress remoteAddr, int remotePort)
DatagramPacket(byte[ ] data, int offset, int length, InetAddress remoteAddr, int
remotePort)
DatagramPacket(byte[ ] data, int length, SocketAddress sockAddr)
DatagramPacket(byte[ ] data, int offset, int length, SocketAddress sockAddr)
以上构造函数都创建一个数据部分包含在指定的字节数组中的数据报文,前两种形式的构造函数主要用来创建接收的端的DatagramPackets实例,因为没有指定其目的地址(尽管可以通过setAddress() 和setPort()方法,或setSocketAddress()方法来指定)。后四种形式主要用来创建发送端的DatagramPackets实例。如果指定了offset,数据报文的数据部分将从字节数组的指定位置发送或接收数据。length参数指定了字节数组中在发送时要传输的字节数,或在接收数据时所能接收的最多字节数。length参数可能比小,但不能比它大。
DatagramPacket: 地址处理
InetAddress getAddress()
void setAddress(InetAddress address)
int getPort()
void setPort(int port)
SocketAddress getSocketAddress()
void setSocketAddress(SocketAddress sockAddr)
DatagramPacket: 处理数据
int getLength()
void setLength(int length)
int getOffset()
byte[ ] getData()
void setData(byte[ ] data)
void setData(byte[ ] buffer, int offset, int length)
UDP客户端
UDP客户端首先向被动等待联系的服务器端发送一个数据报文。一个典型的UDP客户端主要执行以下三步:
1. 创建一个DatagramSocket实例,可以选择对本地地址和端口号进行设置。
2. 使用DatagramSocket类的send() 和 receive()方法来发送和接收DatagramPacket实例,进行通信。
3. 通信完成后,使用DatagramSocket类的close()方法来销毁该套接字。
与Socket类不同,DatagramSocket实例在创建时并不需要指定目的地址。这也是TCP协议和UDP协议的最大不同点之一。在进行数据交换前,TCP套接字必须跟特定主机和另一个端口号上的TCP套接字建立连接,之后,在连接关闭前,该套接字就只能与相连接的那个套接字通信。而UDP套接字在进行通信前则不需要建立连接,每个数据报文都可以发送到或接收于不同的目的地址。(DatagramSocket类的connect()方法确实允许指定远程地址和端口,但该功能是可选的。)
使用UDP协议的一个后果是数据报文可能丢失。在我们的回馈协议中,客户端的回馈请求信息和服务器端的响应信息都有可能在网络中丢失。回顾前面所介绍的TCP回馈客户端,其发送了一个回馈字符串后,将在read()方法上阻塞等待响应。如果试图在我们的UDP回馈客户端上使用相同的策略,数据报文丢失后,我们的客户端就会永远阻塞在receive()方法上。为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来指定receive()方法的最长阻塞时间,因此,如果超过了指定时间仍未得到响应,客户端就会重发回馈请求。我们的回馈客户端执行以下步骤:
1. 向服务器端发送回馈字符串。
2. 在receive()方法上最多阻塞等待3秒钟,在超时前若没有收到响应,则重发请求(最多重发5次)。
3. 终止客户端。
(UDP版本的回馈客户端,在客户端使用DatagramSocket类的setSoTimeout()方法来指定receive()方法的最长阻塞时间,因此,如果超过了指定时间仍未得到响应,客户端就会重发回馈请求)
[java] view plaincopy
1. import amSocket;
2. import amPacket;
3. import dress;
4. import ption;
5. import uptedIOException;
6.
7. public class UDPEchoClientTimeout {
8.
9. private static final int TIMEOUT = 3000; // Resend timeout (milliseconds)
10. private static final int MAXTRIES = 5; // Maximum retransmissions
11.
12. public static void main(String[] args) throws IOException {
13. if (( < 2) || ( > 3)) { // Test for correct #
of args
14. throw new IllegalArgumentException("Parameter(s):
15. }
16. InetAddress serverAddress = ame(args[0]); // Server address
17. // Convert the argument String to bytes using the default encoding
18. byte[] bytesToSend = args[1].getBytes();
19. int servPort = ( == 3) ? nt(args[2]) : 7;
20. DatagramSocket socket = new DatagramSocket();
21. imeout(TIMEOUT); // Maximum receive blocking time (milliseconds)
22. DatagramPacket sendPacket = new DatagramPacket(bytesToSend,, serverAddress, servPort); // Sending packet
23. DatagramPacket receivePacket =new DatagramPacket(new byte[], );// Receiving packet
24. int tries = 0; // Packets may be lost, so we have to keep trying
25. boolean receivedResponse = false;
26. do {
27. (sendPacket); // Send the echo string
28. try {
29. e(receivePacket); // Attempt echo reply reception
30. if (!ress().equals(serverAddress)) {// Check source
31. throw new IOException("Received packet from an unknown source");
32. }
33. receivedResponse = true;
34. } catch (InterruptedIOException e) { // We did not get anything
35. tries += 1;
36. n("Timed out, " + (MAXTRIES - tries)+ " ");
37. }
38. } while ((!receivedResponse) && (tries < MAXTRIES));
39. if (receivedResponse) {
40. n("Received: "+ new String(a()));
41. } else {
42. n("No response -- giving up.");
43. }
44. ();
45. }
46. }
receive()方法将阻塞等待,直到收到一个数据报文或等待超时。超时信息由InterruptedIOException异常指示。一旦超时,发送尝试计数器(tries))加1,并重新发送。若尝试了最大次数后,仍没有接收到数据报文,循环将退出。如果receive()方法成功接收了数据,我们将循环标记receivedResponse设为true,以退出循环。由于数据报文可能发送自任何地址,我们需要验证所接收的数据报文,检查其源地址和端口号是否与所指定的回馈服务器地址和端口号相匹配。
DatagramSocket: 创建
DatagramSocket()
DatagramSocket(int localPort)
DatagramSocket(int localPort, InetAddress localAddr)
以上构造函数将创建一个UDP套接字。可以分别或同时设置本地端口和地址。如果没有指定本地端口,或将其设置为0,该套接字将与任何可用的本地端口绑定。如果没有指定本地地址, 数据包(packet)可以接收发送向任何本地地址的数据报文。
DatagramSocket: 连接与关闭
void connect(InetAddress remoteAddr, int remotePort)
void connect(SocketAddress remoteSockAddr)
void disconnect()
void close()
connect()方法用来设置套接字的远程地址和端口。一旦连接成功,该套接字就只能与指定的地址和端口进行通信,任何向其他地址和端口发送数据报文的尝试都将抛出一个异常。套接字也将只接收从指定地址和端口发送来的数据报文,从其他地址或端口发送来的数据报文将被忽略。重点提示:连接到多播地址或广播地址的套接字只能发送数据报文,因为数据报文的源地址总是一个单播地址
DatagramSocket: 地址处理
InetAddress getInetAddress()
int getPort()
SocketAddress getRemoteSocketAddress()
InetAddress getLocalAddress()
int getLocalPort()
SocketAddress getLocalSocketAddress()
DatagramSocket: 发送和接收
void send(DatagramPacket packet)
void receive(DatagramPacket packet)
receive()方法将阻塞等待,直到接收到数据报文,并将报文中的数据复制到指定的DatagramPacket实例中。
DatagramSocket: 选项
int getSoTimeout()
void setSoTimeout(int timeoutMillis)
以上方法分别获取和设置该套接字中receive()方法调用的最长阻塞时间。如果在接收到数据之前超时,则抛出InterruptedIOException异常。超时时间以毫秒为单位。
UDP服务器端
与TCP服务器一样,UDP服务器的工作是建立一个通信终端,并被动等待客户端发起连接。但由于UDP是无连接的,UDP通信通过客户端的数据报文初始化,并没有TCP中建立连接那一步。典型的UDP服务器要执行以下三步:
1. 创建一个DatagramSocket实例,指定本地端口号,并可以选择指定本地地址。此时,服务器已经准备好从任何客户端接收数据报文。
2. 使用DatagramSocket类的receive()方法来接收一个DatagramPacket实例。当
receive()方法返回时,数据报文就包含了客户端的地址,这样我们就知道了回复信息应该发送到什么地方。
3. 使用DatagramSocket类的send() 和receive()方法来发送和接收DatagramPackets实例,进行通信。
(UDP版本的回馈服务器。非常简单:它不停地循环,接收数据报文后将相同的数据报文返回给客户端,规定:我们的服务器只接收和发送数据报文中的前255(ECHOMAX)个字符,超出的部分将在套接字的具体实现中无提示地丢弃。)
[java] view plaincopy
1. import ption;
2. import amPacket;
3. import amSocket;
4.
5. public class UDPEchoServer {
6. private static final int ECHOMAX = 255; // Maximum size of echo datagram
7.
8. public static void main(String[] args) throws IOException {
9. if ( != 1) { // Test for correct argument list
10. throw new IllegalArgumentException("Parameter(s):
11. }
12. int servPort = nt(args[0]);
13. DatagramSocket socket = new DatagramSocket(servPort);
14. DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
15. while (true) { // Run forever, receiving and echoing datagrams
16. e(packet); // Receive packet from client
17. n("Handling client at "
18. + ress().getHostAddress() + " on port "
19. + t());
20. (packet); // Send the same packet back to client
21. gth(ECHOMAX); // Reset length to avoid shrinking buffer
22. }
23. /* NOT REACHED */
24. }
25. }
当在TCP套接字的输出流上调用的write()方法返回后,所有的调用者都知道数据已经被复制到一个传输缓存区中,实际上此时数据可能已经被传送,也可能还没有被传送。而UDP
协议没有提供从网络错误中恢复的机制,因此,并不对可能需要重传的数据进行缓存。这就意味着,当send()方法调用返回时,消息已经被发送到了底层的传输信道中,并正处在(或即将处在)发送途中。
消息从网络到达后,其所包含数据被read()方法或receive()方法返回前,数据存储在一个先进先出(first-in, first-out,FIFO)的接收数据队列中。对于已连接的TCP套接字来说,所有已接收但还未传送的字节都看作是一个连续的字节序列(见第6章)。然而,对于UDP套接字来说,接收到的数据可能来自于不同的发送者。一个UDP套接字所接收的数据存放在一个消息队列中,每个消息都关联了其源地址信息。每次receive()调用只返回一条消息。然而,如果receive()方法在一个缓存区大小为n的DatagramPacket实例中调用,而接收队列中的第一条消息长度大于n,则receive()方法只返回这条消息的前n个字节。超出部分的其他字节都将自动被丢弃,而且对接收程序也没有任何消息丢失的提示!出于这个原因,接收者应该提供一个有足够大的缓存空间的DatagramPacket实例,以完整地存放调用receive()方法时应用程序协议所允许的最大长度的消息。这个技术能够保证数据不会丢失。一个DatagramPacket实例中所运行传输的最大数据量为65507字节,即UDP数据报文所能负载的最多数据。因此,使用一个有65600字节左右缓存数组的数据包总是安全的。
每一个DatagramPacket实例都包含一个内部消息长度值,而该实例一接收到新消息,这个长度值都可能改变(以反映实际接收的消息的字节数)。如果一个应用程序使用同一个DatagramPacket实例多次调用receive()方法,每次调用前就必须显式地将消息的内部长度重置为缓存区的实际长度。另一个潜在的问题根源是DatagramPacket类的getData()方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。消息接收到DatagramPacket的缓存区时,只是修改了存放消息数据的地址。在Java1.6中我们可以使用Range()方法,只需要一步就能方便地实现以上功能:byte[]
destBuf = Range(a(),set(),
set()+gth());
3:发送和接收数据
任何要交换信息的程序之间在信息的编码方式上必须达成共识(如将信息表示为位序列),以及哪个程序发送信息,什么时候和怎样接收信息都将影响程序的行为。程序间达成的这种包含了信息交换的形式和意义的共识称为协议,用来实现特定应用程序的协议叫做应用程序协议,客户端和服务器的行为都要依赖于它们所交换的信息,因此应用程序协议通常更加复杂。
大部分的应用程序协议是根据由字段序列组成的离散信息定义的,其中每个字段中都包含了一段以位序列编码的特定的信息。应用程序协议中明确定义了信息的发送者应该怎样排列和解释这些位序列,同时还要定义接收者应该怎样解析,这样才使信息的接收者能够抽取出每个字段的意义。TCP/IP协议的唯一约束是,信息必须在块(chunks)中发送和接收,而块的长度必须是8位的倍数,因此,我们可以认为在TCP/IP协议中传输的信息是字节序列。鉴于此,我们可以进一步把传输的信息看作数字序列或数组,每个数字的取值范围是0到255。
信息编码
OutputStream、InputStream、DatagramPacket实例中所能处理的唯一数据类型是字节和字节数组。作为一种强类型语言,Java需要把其他数据类型(int,String等)显式转换成字节数组。
(1)使用"位操作(bit-diddling)"将消息的正确值存入字节数组
[java] view plaincopy
1. public class BruteForceCoding {
2. private static byte byteVal = 101; // one hundred and one
3. private static short shortVal = 10001; // ten thousand and one
4. private static int intVal = 100000001; // one hundred million and one
5. private static long longVal = 1L;// one trillion and one
6.
7. private final static int BSIZE = / ;
8. private final static int SSIZE = / ;
9. private final static int ISIZE = / ;
10. private final static int LSIZE = / ;
11.
12. private final static int BYTEMASK = 0xFF; // 8 bits
13.
14. public static String byteArrayToDecimalString(byte[] bArray) {
15. StringBuilder rtn = new StringBuilder();
16. for (byte b : bArray) {
17. (b & BYTEMASK).append(" ");
18. }
19. return ng();
20. }
21.
22. // Warning: Untested preconditions (e.g., 0 <= size <= 8)
23. public static int encodeIntBigEndian(byte[] dst, long val, int offset,
24. int size) {
25. for (int i = 0; i < size; i++) {
26. dst[offset++] = (byte) (val >> ((size - i - 1) * ));
27. }
28. return offset;
29. }
30.
31. // Warning: Untested preconditions (e.g., 0 <= size <= 8)
32. public static long decodeIntBigEndian(byte[] val, int offset, int size)
{
33. long rtn = 0;
34. for (int i = 0; i < size; i++) {
35. rtn = (rtn << ) | ((long) val[offset + i] & BYTEMASK);
36. }
37. return rtn;
38. }
39.
40. public static void main(String[] args) {
41. byte[] message = new byte[BSIZE + SSIZE + ISIZE + LSIZE];
42. // Encode the fields in the target byte array
43. int offset = encodeIntBigEndian(message, byteVal, 0, BSIZE);
44. offset = encodeIntBigEndian(message, shortVal, offset, SSIZE);
45. offset = encodeIntBigEndian(message, intVal, offset, ISIZE);
46. encodeIntBigEndian(message, longVal, offset, LSIZE);
47. n("Encoded message: "+ byteArrayToDecimalString(message));
48.
49. // Decode several fields
50. long value = decodeIntBigEndian(message, BSIZE, SSIZE);
51. n("Decoded short = " + value);
52. value = decodeIntBigEndian(message, BSIZE + SSIZE + ISIZE, LSIZE);
53. n("Decoded long = " + value);
54.
55. // Demonstrate dangers of conversion
56. offset = 4;
57. value = decodeIntBigEndian(message, offset, BSIZE);
58. n("Decoded value (offset " + offset + ", size "+ BSIZE + ") = " + value);
59. byte bVal = (byte) decodeIntBigEndian(message, offset, BSIZE);
60. n("Same value as byte = " + bVal);
61. }
62.
63. }
上面的强制(brute-force)编码方法需要程序员做很多工作:要计算和命名每个数值的偏移量和大小,并要为编码过程提供合适的参数。如果没有将encodeIntBigEndian()方法提出来作为一个独立的方法,情况会更糟。基于以上原因,强制编码方法是不推荐使用的,而且Java也提供了一些更加易用的内置机制。不过,值得注意的是强制编码方法也有它的优势,除了能够对标准的Java整型进行编码外,encodeIntegerBigEndian() 方法对1到8字节的任何整数都适用--例如,如果愿意的话,你可以对一个7字节的整数进行编码。
(2)使用Java的内置工具将消息的正确值存入字节数组
所幸的是Java的内置工具能够帮助我们完成这些转换。如String类的getBytes()方法,该方法就是将一个Sring实例中的字符转换成字节的标准方式,如DataOutputStream类和ByteArrayOutputStre类,DataOutputStream 类允许你将基本数据类型,如整型,写入一个流中:它提供了writeByte(),writeShort(),writeInt(),以及writeLong()方法,这些方
法按照big-endian顺序,将整数以适当大小的二进制补码的形式写到流中。ByteArrayOutputStream类获取写到流中的字节序列,并将其转换成一个字节数组。用这两个类来构建我们的消息的代码:
static byte byteVal = 101; // one hundred and one
static short shortVal = 10001; // ten thousand and one
static int intVal = 100000001; // onehundred million and one
static long longVal = 1L;// one trillion and one
ByteArrayOutputStream buf = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(buf);
yte(byteVal);
hort(shortVal);
nt(intVal);
ong(longVal);
();
byte[] msg = Array();
接收方将如何恢复传输的数据呢?正如你想的那样,Java中也提供了与输出工具类相似的输入工具类,分别是DataInputStream类和ByteArrayInputStream类。
基本整型
发送者和接收者必须先在一些方面达成共识。
(1)要传输的每个整数的字节大小(size)
Java程序中,int数据类型由32位表示,因此,我们可以使用4个字节来传输任意的int型变量或常量;short数据类型由16位表示,传输short类型的数据只需要两个字节;同理,传输64位的long类型数据则需要8个字节。
(2)字节的发送顺序
有两种选择:从整数的右边开始,由低位到高位地发送,即little-endian顺序;或从左边开始,由高位到低位发送,即big-endian顺序。对于任何多字节的整数,发送者和接收者必须在使用big-endian顺序还是使用little-endian顺序上达成共识。(注意,幸运的是字节中位的顺序在实现时是以标准的方式处理的,以big-endian顺序为主)
(3)所传输的数值是有符号的(signed)还是无符号的(unsigned)
Java中的四种基本整型都是有符号的,它们的值以二进制补码(two's-complement)的方式存储,由于Java并不支持无符号整型,如果要在Java中编码和解码无符号数,则需要做一点额外的工作。
字符串和文本
发送者与接收者必须在符号与整数的映射方式上即字符集编码达成共识,,在一组符号与一组整数之间的映射称为编码字符集(coded character set.),Java使用了一种称为Unicode的国际标准编码字符集来表示char型和String型值。Unicode字符集将"世界上大部分的语言和符号映射到整数0至65535之间,能更好地适用于国际化程序。Unicode包含了ASCII码:每个ASCII码中定义的符号在Unicode中所映射整数与其在ASCII码中映射的整数相同。这就为ASCII与Unicode之间提供了一定程度的向后兼容性。
组合输入输出流
Java中与流相关的类可以组合起来从而提供强大的功能。例如,我们可以将一个Socket
实例的OutputStream包装在一个BufferedOutputStream实例中,这样可以先将字节暂时缓存在一起,然后再一次全部发送到底层的通信信道中,以提高程序的性能。我们还能再将这个BufferedOutputStream实例包裹在一个DataOutputStream实例中,以实现发送基本数据类型的功能。以下是实现这种组合的代码:
Socket socket = new Socket(server, port);
DataOutputStream out = new DataOutputStream( new
BufferedOutputStream(putStream()));
在这个例子中,我们先将基本数据的值,一个一个写入DataOutputStream中,DataOutputStream再将这些数据以二进制的形式写入BufferedOutputStream将三次写入的数据缓存起来,然后再由BufferedOutputStream一次性地将这些数据写入套接字的OutputStream,最后由OutputStream将数据发送到网络。在另一个终端,我们创建了相应的组合InputStream,以有效地接收基本数据类型。
成帧与解析
将数据转换成在线路上传输的格式只完成了一半工作,在接收端还必须将接收到的字节序列还原成原始信息。应用程序协议通常处理的是由一组字段组成的离散的信息。成帧(Framing)技术则解决了接收端如何定位消息的首尾位置的问题。无论信息是编码成了文本、多字节二进制数、或是两者的结合,应用程序协议必须指定消息的接收者如何确定何时消息已完整接收。
如果一条完整的消息负载在一个DatagramPacket中发送,这个问题就变得很简单了:DatagramPacket 负载的数据有一个确定的长度,接收者能够准确地知道消息的结束位置。然而,如果通过TCP套接字来发送消息,情况将变得更复杂,因为TCP协议中没有消息边界的概念。如果一个消息中的所有字段都有固定的长度,同时每个消息又是由固定数量的字段组成的话,消息的长度就能够确定,接收者就可以简单地将消息长度对应的字
节数读到一个byte[]缓存区中。但是如果消息的长度是可变的(例如消息中包含了一些变长的文本字符串),我们事先就无法知道需要读取多少字节。
如果接收者试图从套接字中读取比消息本身更多的字节,将可能发生以下两种情况之一:如果信道中没有其他消息,接收者将阻塞等待,同时无法处理接收到的消息;如果发送者也在等待接收端的响应信息,则会形成死锁(deadlock);另一方面,如果信道中还有其他消息,则接收者会将后面消息的一部分甚至全部读到第一条消息中去,这将产生一些协议错误。因此,在使用TCP套接字时,成帧就是一个非常重要的考虑因素。
一些相同的考虑也适用于查找消息中每个字段的边界:接收者需要知道每个字段的结束位置和下一个字段的开始位置。因此,我们在此介绍的消息成帧技术几乎都可以应用到字段上。然而,最简单并使代码最简洁的方法是将这两个问题分开处理:首先定位消息的结束位置,然后将消息作为一个整体进行解析。
主要有两个技术使接收者能够准确地找到消息的结束位置:
(1)基于定界符(Delimiter-based):消息的结束由一个唯一的标记(unique marker,)指出,即发送者在传输完数据后显式添加的一个特殊字节序列。这个特殊标记不能在传输的数据中出现。
(2)显式长度(Explicit length):在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节。
基于定界符的方法通常用在以文本方式编码的消息中:定义一个特殊的字符或字符串来标识消息的结束。接收者只需要简单地扫描输入信息(以字节的方式)来查找定界序列,并将定界符前面的字符串返回。这种方法的缺点是消息本身不能包含有定界字符,否则接收者将提前认为消息已经结束。在基于定界符的成帧方法中,发送者要保证满足这个先决条件。缺点是发送者和接收者双方都必须扫描消息。
基于长度的方法更简单一些,不过要使用这种方法必须知道消息长度的上限。发送者先要确定消息的长度,将长度信息存入一个整数,作为消息的前缀。消息的长度上限定义了用来编码消息长度所需要的字节数:如果消息的长度小于256字节,则需要1个字节;如果消息的长度小于65536字节,则需要2个字节,等等。
为了展示以上技术,我们将介绍下面定义的Framer接口。它有两个方法:frameMsg()方法用来添加成帧信息并将指定消息输出到指定流,nextMsg()方法则扫描指定的流,从中抽取出下一条消息。
[java] view plaincopy
1. import ption;
2. import Stream;
3.
4. public interface Framer {
5. void frameMsg(byte[] message, OutputStream out) throws IOException;
6. byte[] nextMsg() throws IOException;
7. }
类实现了基于定界符的成帧方法,其定界符为"换行"符("n", 字节值为10)。 frameMethod()方法并没有实现填充,当成帧的字节序列中包含有定界符时,它只是简单地抛出异常。(扩展该方法以实现填充功能:结束符n,数据中的n-->ESCy,数据中的ESC-->ESCz,避免数据中碰巧出现ESCy时而被误转化为n,ESC被称为转义符)nextMsg()方法扫描流,直到读取到了定界符,并返回定界符前面的所有字符,如果流为空则返回null。如果累积了一个消息的不少字符,但直到流结束也没有找到定界符,程序将抛出一个异常来指示成帧错误。
[java] view plaincopy
1. import rayOutputStream;
2. import eption;
3. import ption;
4. import tream;
5. import Stream;
6.
7. public class DelimFramer implements Framer {
8.
9. private InputStream in; // data source
10. private static final byte DELIMITER = "n"; // message delimiter
11.
12. public DelimFramer(InputStream in) {
13. = in;
14. }
15.
16. public void frameMsg(byte[] message, OutputStream out) throws IOException {
17. // ensure that the message does not contain the delimiter
18. for (byte b : message) {
19. if (b == DELIMITER) {
20. throw new IOException("Message contains delimiter");
21. }
22. }
23. (message);
24. (DELIMITER);
25. ();
26. }
27.
28. public byte[] nextMsg() throws IOException {
29. ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
30. int nextByte;
31. // fetch bytes until find delimiter
32. while ((nextByte = ()) != DELIMITER) {
33. if (nextByte == -1) { // end of stream?
34. if (() == 0) { // if no byte read
35. return null;
36. } else { // if bytes followed by end of stream: framing error
37. throw new EOFException("Non-empty message without delimiter");
38. }
39. }
40. (nextByte); // write byte to buffer
41. }
42. return Array();
43. }
44. }
类实现了基于长度的成帧方法,适用于长度小于65535 (216 ? 1)字节的消息。发送者首先给出指定消息的长度,并将长度信息以big-endian顺序存入两个字节的整数中,再将这两个字节放在完整的消息内容前,连同消息一起写入输出流。在接收端,我们使用DataInputStream以读取整型的长度信息;readFully() 方法将阻塞等待,直到给定的
数组完全填满,这正是我们需要的。值得注意的是,使用这种成帧方法,发送者不需要检查要成帧的消息内容,而只需要检查消息的长度是否超出了限制。
[java] view plaincopy
1. import putStream;
2. import eption;
3. import ption;
4. import tream;
5. import Stream;
6.
7. public class LengthFramer implements Framer {
8. public static final int MAXMESSAGELENGTH = 65535;
9. public static final int BYTEMASK = 0xff;
10. public static final int SHORTMASK = 0xffff;
11. public static final int BYTESHIFT = 8;
12.
13. private DataInputStream in; // wrapper for data I/O
14.
15. public LengthFramer(InputStream in) throws IOException {
16. = new DataInputStream(in);
17. }
18.
19. public void frameMsg(byte[] message, OutputStream out) throws IOException {
20. if ( > MAXMESSAGELENGTH) {
21. throw new IOException("message too long");
22. }
23. // write length prefix
24. (( >> BYTESHIFT) & BYTEMASK);//(int b)只写b的低位8bit到流中,高24bit忽略
25. ( & BYTEMASK);
26. // write message
27. (message);
28. ();
29. }
30.
31. public byte[] nextMsg() throws IOException {
32. int length;
33. try {
34. length = signedShort(); // read 2 bytes
35. } catch (EOFException e) { // no (or 1 byte) message
36. return null;
37. }
38. // 0 <= length <= 65535
39. byte[] msg = new byte[length];
40. lly(msg); // if exception, it's a framing error.
41. return msg;
42. }
43. }
构建和解析协议消息
简单的"投票"协议,如图所示。一个客户端向服务器发送了一个请求消息,消息中包含了一个候选人ID,范围是0至1000。
程序支持两种请求。一种是查询(inquiry),即向服务器询问给定候选人当前获得的投票总数。服务器发回一个响应消息,包含了原来的候选人ID和该候选人当前(查询请求收到时)获得的选票总数。另一种是投票(voting)请求,即向指定候选人投一票。服务器对这种请求也发回响应消息,包含了候选人ID和其获得的选票数(包括了刚投的一票)。
在实现一个协议时,定义一个专门的类来存放消息中所包含的信息是大有裨益的。该类提供了操作消息中的字段的方法--同时用来维护不同字段之间的不变量。在我们的例子中,客户端和服务器端发送的消息都非常简单,它们唯一的区别是服务器端发送的消息包含了选票总数和一个表示响应消息(不是请求消息)的标志。因此,我们可以用一个类来表示客户端和服务器端的两种消息。
消息类VoteMsg(展示了每条消息中的基本信息)
布尔值isInquiry,其值为true时表示该消息是查询请求(为false时表示该消息是投票信息);
布尔值isResponse,指示该消息是响应(由服务器发送)还是请求;
整型变量candidateID指示了候选人的ID;
长整型变量voteCount指示出所查询的候选人获得的总选票数。
这个类还维护了以下字段间的不变量:
candidateID的范围是0到1000。
voteCount在响应消息中只能是一个非零值(isResponse为true)。
voteCount 不能为负数。
[java] view plaincopy
1. public class VoteMsg {
2. private boolean isInquiry; // true if inquiry; false if vote
3. private boolean isResponse;// true if response from server
4. private int candidateID; // in [0,1000]
5. private long voteCount; // nonzero only in response
6.
7. public static final int MAX_CANDIDATE_ID = 1000;
8.
9. public VoteMsg(boolean isResponse, boolean isInquiry, int candidateID,
10. long voteCount) throws IllegalArgumentException {
11. // check invariants
12. if (voteCount != 0 && !isResponse) {
13. throw new IllegalArgumentException("Request vote count must be zero");
14. }
15. if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
16. throw new IllegalArgumentException("Bad Candidate ID: "+ candidateID);
17. }
18. if (voteCount < 0) {
19. throw new IllegalArgumentException("Total must be >= zero");
20. }
21. ateID = candidateID;
22. onse = isResponse;
23. iry = isInquiry;
24. unt = voteCount;
25. }
26.
27. public void setInquiry(boolean isInquiry) {
28. iry = isInquiry;
29. }
30.
31. public void setResponse(boolean isResponse) {
32. onse = isResponse;
33. }
34.
35. public boolean isInquiry() {
36. return isInquiry;
37. }
38.
39. public boolean isResponse() {
40. return isResponse;
41. }
42.
43. public void setCandidateID(int candidateID) throws IllegalArgumentException {
44. if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
45. throw new IllegalArgumentException("Bad Candidate ID: "+ candidateID);
46. }
47. ateID = candidateID;
48. }
49.
50. public int getCandidateID() {
51. return candidateID;
52. }
53.
54. public void setVoteCount(long count) {
55. if ((count != 0 && !isResponse) || count < 0) {
56. throw new IllegalArgumentException("Bad vote count");
57. }
58. voteCount = count;
59. }
60.
61. public long getVoteCount() {
62. return voteCount;
63. }
64.
65. public String toString() {
66. String res = (isInquiry ? "inquiry" : "vote") + " for candidate "+ candidateID;
67. if (isResponse) {
68. res = "response to " + res + " who now has " + voteCount+ " vote(s)";
69. }
70. return res;
71. }
72. }
编码和解码类接口VoteMsgCoder
VoteMsgCoder接口提供了对投票消息进行序列化和反序列化的方法
[java] view plaincopy
1. import ption;
2.
3. public interface VoteMsgCoder {
4. byte[] toWire(VoteMsg msg) throws IOException;
5.
6. VoteMsg fromWire(byte[] input) throws IOException;
7. }
toWire()方法用于根据一个特定的协议,将投票消息转换成一个字节序列,fromWire()方法则根据相同的协议,对给定的字节序列进行解析,并根据信息的内容构造出消息类的一个实例。
为了介绍不同的信息编码方法,我们展示了两个实现VoteMsgCoder接口的类。一个使用的是基于文本的编码方式,另一个使用的是二进制的编码方式。
基于文本的编码、解码类VoteMsgTextCoder
用文本方式对消息进行编码的版本。该协议指定使用US-ASCII字符集对文本进行编码。消息的开头是一个所谓的"魔术字符串",即一个字符序列,用于接收
者快速将投票协议的消息和网络中随机到来的垃圾消息区分开。投票/查询布尔值被编码成字符形式,'v'表示投票消息,'i'表示查询消息。消息的状态,即是否为服务器的响应,由字符'R'指示。状态标记后面是候选人ID,其后跟的是选票总数,它们都编码成十进制字符串。
[java] view plaincopy
1. import rayInputStream;
2. import ption;
3. import treamReader;
4. import r;
5.
6. public class VoteMsgTextCoder implements VoteMsgCoder {
7. /*
8. * Wire Format "VOTEPROTO" <"v"|"i"> [
9. * Charset is fixed by the wire format.
10. */
11. // Manifest constants for encoding
12. public static final String MAGIC = "Voting";
13. public static final String VOTESTR = "v";
14. public static final String INQSTR = "i";
15. public static final String RESPONSESTR = "R";
16. public static final String CHARSETNAME = "US-ASCII";
17. public static final String DELIMSTR = " ";
18. public static final int MAX_WIRE_LENGTH = 2000;
19.
20. public byte[] toWire(VoteMsg msg) throws IOException {
21. String msgString = MAGIC + DELIMSTR
22. + (iry() ? INQSTR : VOTESTR) + DELIMSTR
23. + (onse() ? RESPONSESTR + DELIMSTR : "")
24. + ng(didateID()) + DELIMSTR
25. + ng(eCount());
26. byte data[] = es(CHARSETNAME);
27. return data;
28. }
29.
30. public VoteMsg fromWire(byte[] message) throws IOException {
31. ByteArrayInputStream msgStream = new ByteArrayInputStream(message);
32. Scanner s = new Scanner(new InputStreamReader(msgStream, CHARSETNAME));
33. boolean isInquiry;
34. boolean isResponse;
35. int candidateID;
36. long voteCount;
37. String token;
38. try {
39. token = ();
40. if (!(MAGIC)) {
41. throw new IOException("Bad magic string: " + token);
42. }
43. token = ();
44. if ((VOTESTR)) {
45. isInquiry = false;
46. } else if (!(INQSTR)) {
47. throw new IOException("Bad vote/inq indicator: " + token);
48. } else {
49. isInquiry = true;
50. }
51. token = ();
52. if ((RESPONSESTR)) {
53. isResponse = true;
54. token = ();
55. } else {
56. isResponse = false;
57. }
58. // Current token is candidateID
59. // Note: isResponse now valid
60. candidateID = nt(token);
61. if (isResponse) {
62. token = ();
63. voteCount = ong(token);
64. } else {
65. voteCount = 0;
66. }
67. } catch (IOException ioe) {
68. throw new IOException("");
69. }
70. return new VoteMsg(isResponse, isInquiry, candidateID, voteCount);
71. }
72. }
toWire()方法简单地创建一个字符串,该字符串中包含了消息的所有字段,并由空白符隔开。fromWire()方法首先检查"魔术"字符串,如果在消息最前面没有魔术字符串,则抛出一个异常。这里说明了在实现协议时非常重要的一点:永远不要对从网络来的任何输入进行任何假设。你的程序必须时刻为任何可能的输入做好准备,并能够很好地对其进行处理。在这个例子中,如果接收到的不是期望的消息,fromWire()方法将抛出一个异常,否则,就使用Scanner实例,根据空白符一个一个地获取字段。注意,消息的字段数与其是请求消息(由客户端发送)还是响应消息(由服务器发送)有关。如果输入流提前结束或格式错误,fromWire()方法将抛出一个异常。
基于二进制的编码、解码类VoteMsgBinCoder
与基于文本的格式相反,二进制格式使用固定大小的消息。每条消息由一个特殊字节开始,该字节的最高六位为一个"魔术"值010101。这一点少量的冗余信息为接收者收到适当的投票消息提供了一定程度的保证。该字节的最低两位对两个布尔值进行了编码。消息的第二个字节总是0,第三、第四个字节包含了candidateID值。只有响应消息的最后8个字节才包含了选票总数信息。
[java] view plaincopy
1. import rayInputStream;
2. import rayOutputStream;
3. import putStream;
4. import tputStream;
5. import ption;
6.
7. /* Wire Format
8. * 1 1 1 1 1 1
9. * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
10. * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
11. * | Magic |Flags| ZERO |
12. * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
13. * | Candidate ID |
14. * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
15. * | |
16. * | Vote Count (only in response) |
17. * | |
18. * | |
19. * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
20. */
21. public class VoteMsgBinCoder implements VoteMsgCoder {
22. // manifest constants for encoding
23. public static final int MIN_WIRE_LENGTH = 4;
24. public static final int MAX_WIRE_LENGTH = 16;
25. public static final int MAGIC = 0x5400;
26. public static final int MAGIC_MASK = 0xfc00;
27. public static final int MAGIC_SHIFT = 8;
28. public static final int RESPONSE_FLAG = 0x0200;
29. public static final int INQUIRE_FLAG = 0x0100;
30.
31. public byte[] toWire(VoteMsg msg) throws IOException {
32. ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
33. DataOutputStream out = new DataOutputStream(byteStream); // converts
ints
34.
35. short magicAndFlags = MAGIC;
36. if (iry()) {
37. magicAndFlags |= INQUIRE_FLAG;
38. }
39. if (onse()) {
40. magicAndFlags |= RESPONSE_FLAG;
41. }
42. hort(magicAndFlags);
43. // We know the candidate ID will fit in a short: it's > 0 && < 1000
44. hort((short) didateID());
45. if (onse()) {
46. ong(eCount());
47. }
48. ();
49. byte[] data = Array();
50. return data;
51. }
52.
53. public VoteMsg fromWire(byte[] input) throws IOException {
54. // sanity checks
55. if ( < MIN_WIRE_LENGTH) {
56. throw new IOException("Runt message");
57. }
58. ByteArrayInputStream bs = new ByteArrayInputStream(input);
59. DataInputStream in = new DataInputStream(bs);
60. int magic = ort();
61. if ((magic & MAGIC_MASK) != MAGIC) {
62. throw new IOException("Bad Magic #: "+ ((magic & MAGIC_MASK) >>
MAGIC_SHIFT));
63. }
64. boolean resp = ((magic & RESPONSE_FLAG) != 0);
65. boolean inq = ((magic & INQUIRE_FLAG) != 0);
66. int candidateID = ort();
67. if (candidateID < 0 || candidateID > 1000) {
68. throw new IOException("Bad candidate ID: " + candidateID);
69. }
70. long count = 0;
71. if (resp) {
72. count = ng();
73. if (count < 0) {
74. throw new IOException("Bad vote count: " + count);
75. }
76. }
77. // Ignore any extra bytes
78. return new VoteMsg(resp, inq, candidateID, count);
79. }
80. }
服务器中记录投票过程的服务类VoteService
通过流发送消息非常简单,只需要创建消息,调用toWire()方法,添加适当的成帧信息,再写入流。当然,接收消息就要按照相反的顺序执行。这个过程适用于TCP协议,而对于UDP协议,不需要显式地成帧,因为UDP协议中保留了消息的边界信息。为了对发送与接收过程进行展示,我们考虑投票服务的如下几点:1)维护一个候选人ID与其获得选票数的映射,2)记录提交的投票,3)根据其获得的选票数,对查询指定的候选人和为其投票的消息做出响应。首先,我们实现一个投票服务器所用到的服务。当接收到投票消息时,投票服务器将调用VoteService类的handleRequest() 方法对请求进行处理。
[java] view plaincopy
1. import p;
2. import ;
3.
4. public class VoteService {
5. // Map of candidates to number of votes
6. private Map
7.
8. public VoteMsg handleRequest(VoteMsg msg) {
9. if (onse()) { // If response, just send it back
10. return msg;
11. }
12. ponse(true); // Make message a response
13. // Get candidate ID and vote count
14. int candidate = didateID();
15. Long count = (candidate);
16. if (count == null) {
17. count = 0L; // Candidate does not exist
18. }
19. if (!iry()) {
20. (candidate, ++count); // If vote, increment count
21. }
22. eCount(count);
23. return msg;
24. }
25. }
26.
TCP投票客户端类VoteClientTCP
该客户端通过TCP套接字连接到投票服务器,在一次投票后发送一个查询请求,并接收查询和投票结果。
发送:消息对象-->编码/解码对象将消息对象编码成字节数组-->成帧/解帧对象将字节数组成帧后通过输出流发送
接收:成帧/解帧对象将接收到的输入流解帧成字节数组-->编码/解码对象将字节数组解码成消息对象-->消息对象
[java] view plaincopy
1. import Stream;
2. import ;
3.
4. public class VoteClientTCP {
5. public static final int CANDIDATEID = 888;
6.
7. public static void main(String args[]) throws Exception {
8. if ( != 2) { // Test for correct # of args
9. throw new IllegalArgumentException("Parameter(s):
10. }
11. String destAddr = args[0]; // Destination address
12. int destPort = nt(args[1]); // Destination port
13.
14. Socket sock = new Socket(destAddr, destPort);
15. OutputStream out = putStream();
16. // Change Bin to Text for a different framing strategy
17. VoteMsgCoder coder = new VoteMsgBinCoder();
18. // Change Length to Delim for a different encoding strategy
19. Framer framer = new LengthFramer(utStream());
20. // Create an inquiry request (2nd arg = true)
21. VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0);
22. byte[] encodedMsg = (msg);
23.
24. // Send request
25. n("Sending Inquiry (" + + " bytes): ");
26. n(msg);
27. sg(encodedMsg, out);
28.
29. // Now send a vote
30. uiry(false);
31. encodedMsg = (msg);
32. n("Sending Vote (" + + " bytes):
");
33. sg(encodedMsg, out);
34.
35. // Receive inquiry response
36. encodedMsg = g();
37. msg = re(encodedMsg);
38. n("Received Response (" + + " bytes): ");
39. n(msg);
40.
41. // Receive vote response
42. msg = re(g());
43. n("Received Response (" + + " bytes): ");
44. n(msg);
45.
46. ();
47. }
48. }
TCP投票服务器端类VoteServerTCP
该服务器反复地接收新的客户端连接,并使用VoteService类为客户端的投票消息作出响应。
[java] view plaincopy
1. import ption;
2. import Socket;
3. import ;
4.
5. public class VoteServerTCP {
6.
7. public static void main(String args[]) throws Exception {
8. if ( != 1) { // Test for correct # of args
9. throw new IllegalArgumentException("Parameter(s):
10. }
11. int port = nt(args[0]); // Receiving Port
12. ServerSocket servSock = new ServerSocket(port);
13. // Change Bin to Text on both client and server for different encoding
14. VoteMsgCoder coder = new VoteMsgBinCoder();
15. VoteService service = new VoteService();
16. while (true) {
17. Socket clntSock = ();
18. n("Handling client at "+ oteSocketAddress());
19. // Change Length to Delim for a different framing strategy
20. Framer framer = new LengthFramer(utStream());
21. try {
22. byte[] req;
23. while ((req = g()) != null) {
24. n("Received message (" + + "
bytes)");
25. VoteMsg responseMsg = Request(re(req));
26. sg((responseMsg), putStream());
27. }
28. } catch (IOException ioe) {
29. n("Error handling client: " + sage());
30. } finally {
31. n("Closing connection");
32. ();
33. }
34. }
35. }
36. }
UDP投票客户端类VoteClientUDP
UDP版本的投票客户端与TCP版本非常相似。需要注意的是,在UDP客户端中我们不需要使用成帧器,因为UDP协议为我们维护了消息的边界信息。对于UDP协议,我们使用基于文本的编码方式对消息进行编码,不过只要客户端与服务器能达成一致,也能够很方便地改成其他编码方式。
[java] view plaincopy
1. import ption;
2. import amPacket;
3. import amSocket;
4. import dress;
5. import ;
6.
7. public class VoteClientUDP {
8.
9. public static void main(String args[]) throws IOException {
10. if ( != 3) { // Test for correct # of args
11. throw new IllegalArgumentException("Parameter(s):
12. }
13. InetAddress destAddr = ame(args[0]); // Destination addr
14. int destPort = nt(args[1]); // Destination port
15. int candidate = nt(args[2]); // 0 <= candidate <= 1000req'd
16. DatagramSocket sock = new DatagramSocket(); // UDP socket for sending
17. t(destAddr, destPort);
18. // Create a voting message (2nd param false = vote)
19. VoteMsg vote = new VoteMsg(false, false, candidate, 0);
20. // Change Text to Bin here for a different coding strategy
21. VoteMsgCoder coder = new VoteMsgTextCoder();
22. // Send request
23. byte[] encodedVote = (vote);
24. n("Sending Text-Encoded Request ("+ + " bytes): ");
25. n(vote);
26. DatagramPacket message = new DatagramPacket(encodedVote,);
27. (message);
28. // Receive response
29. message = new DatagramPacket(new byte[_WIRE_LENGTH],_WIRE_LENGTH);
30. e(message);
31. encodedVote = Range(a(), 0, gth());
32. n("Received Text-Encoded Response ("+ + " bytes): ");
33. vote = re(encodedVote);
34. n(vote);
35. }
36. }
UDP投票服务器端类VoteServerUDP
UDP投票服务器,同样,也与TCP版本非常相似。
[java] view plaincopy
1. import ption;
2. import amPacket;
3. import amSocket;
4. import ;
5.
6. public class VoteServerUDP {
7.
8. public static void main(String[] args) throws IOException {
9.
10. if ( != 1) { // Test for correct # of args
11. throw new IllegalArgumentException("Parameter(s):
12. }
13. int port = nt(args[0]); // Receiving Port
14. DatagramSocket sock = new DatagramSocket(port); // Receive socket
15. byte[] inBuffer = new byte[_WIRE_LENGTH];
16. // Change Bin to Text for a different coding approach
17. VoteMsgCoder coder = new VoteMsgTextCoder();
18. VoteService service = new VoteService();
19. while (true) {
20. DatagramPacket packet = new DatagramPacket(inBuffer,);
21. e(packet);
22. byte[] encodedMsg = Range(a(), 0, gth());
23. n("Handling request from "+ ketAddress() + " (" + + " bytes)");
24. try {
25. VoteMsg msg = re(encodedMsg);
26. msg = Request(msg);
27. a((msg));
28. n("Sending response (" + gth()+ " bytes):");
29. n(msg);
30. (packet);
31. } catch (IOException ioe) {
32. n("Parse error in message: "+ sage());
33. }
34. }
35. }
36. }
4:多任务处理
"迭代服务器(iterative server)":按顺序处理客户端的请求,也就是说在完成了对前一客户端的服务后,才会对下一个客户端进行响应。这种服务器最适用于每个客户端所请求的连接时间都被限制在较小范围内的应用中,而对于允许客户端请求长时间服务的情况,后续客户端将面临无法接受的长时间等待。需要一种方法可以独立处理每一个连接,并使它们不会产生相互干扰,而Java的多线程技术刚好满足了这一需求,这一机制使服务器能够方便地同时处理多个客户端的请求。通过使用多线程,一个应用程序可以并行执行多项任务,就好像有多个Java虚拟机在同时运行。(实际上是多个线程共享了同一个Java虚拟机。)在我们的响应服务器中,可以为每个客户端分配一个执行线程来实现。
两种实现并行服务器(concurrent servers)的编程方法:
(1)一客户一线程(thread-per-client),即为每一个客户端连接创建一个新的线程;
(2)线程池(threadpool),即将客户端连接分配给一组事先创建好的线程。
如果客户端的执行过程涉及到需要更新服务器端线程间的共享信息,这将变得相当麻烦。在这种情况下,必须非常小心,以确保不同的线程间在共享数据上得到了妥善的同步,否则,会导致共享信息不一致的状况发生,更麻烦的是这些问题追踪起来还非常困难。
服务器协议类(封装了对每个客户端的处理过程,以回显程序为例)
EchoProtocol中给出了回显协议的代码。这个类的静态方法handleEchoClient()中封装了对每个客户端的处理过程。
[java] view plaincopy
1. import ption;
2. import tream;
3. import Stream;
4. import ;
5. import ;
6. import ;
7.
8. public class EchoProtocol implements Runnable {
9. private static final int BUFSIZE = 32; // Size (in bytes) of I/O buffer
10. private Socket clntSock; // Socket connect to client
11. private Logger logger; // Server logger
12.
13. public EchoProtocol(Socket clntSock, Logger logger) {
14. ck = clntSock;
15. = logger;
16. }
17.
18. public static void handleEchoClient(Socket clntSock, Logger logger) {
19. try {
20. // Get the input and output I/O streams from socket
21. InputStream in = utStream();
22. OutputStream out = putStream();
23. int recvMsgSize; // Size of received message
24. int totalBytesEchoed = 0; // Bytes received from client
25. byte[] echoBuffer = new byte[BUFSIZE]; // Receive Buffer
26. // Receive until client closes connection, indicated by -1
27. while ((recvMsgSize = (echoBuffer)) != -1) {
28. (echoBuffer, 0, recvMsgSize);
29. totalBytesEchoed += recvMsgSize;
30. }
31. ("Client " + oteSocketAddress()+ ", echoed " + totalBytesEchoed + " bytes.");
32.
33. } catch (IOException ex) {
34. (G, "Exception in echo protocol", ex);
35. } finally {
36. try {
37. ();
38. } catch (IOException e) {
39. }
40. }
41. }
42.
43. public void run() {
44. handleEchoClient(clntSock, logger);
45. }
46. }
一客户一线程
在一客户一线程(thread-per-client)的服务器中,为每个连接都创建了一个新的线程来处理。服务器循环执行一些任务,在指定端口上侦听连接,反复接收客户端传入的连接请求,并为每个连接创建一个新的线程来对其进行处理。
[java] view plaincopy
1. import ption;
2. import Socket;
3. import ;
4. import ;
5.
6. public class TCPEchoServerThread {
7.
8. public static void main(String[] args) throws IOException {
9.
10. if ( != 1) { // Test for correct # of args
11. throw new IllegalArgumentException("Parameter(s):
12. }
13. int echoServPort = nt(args[0]); // Server port
14. // Create a server socket to accept client connection requests
15. ServerSocket servSock = new ServerSocket(echoServPort);
16. Logger logger = ger("practical");
17. // Run forever, accepting and spawning a thread for each connection
18. while (true) {
19. Socket clntSock = (); // Block waiting for connection
20. // Spawn thread to handle new connection
21. Thread thread = new Thread(new EchoProtocol(clntSock, logger));
22. ();
23. ("Created and started Thread " + e());
24. }
25. /* NOT REACHED */
26. }
27. }
线程池
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
与一客户一线程服务器一样,线程池服务器首先创建一个ServerSocket实例。然后创建N个线程,每个线程都反复循环,从(共享的)ServerSocket实例接收客户端连接。当多个线程同时调用同一个ServerSocket实例的accept()方法时,它们都将阻塞等待,直到一个新的连接成功建立。然后系统选择一个线程,新建立的连接对应的Socket实例则只在选中的线程中返回。其他线程则继续阻塞,直到成功建立下一个连接和选中另一个幸运的线程。由于线程池中的所有线程都反复循环,一个接一个地处理客户端连接,线程池服务器的行为就像是一组迭代服务器。与一客户一线程服务器不同,线程池中的线程在完成对一个客户端的服务后并不终止,相反,它又重新开始在accept()方法上阻塞等待。
[java] view plaincopy
1. import ption;
2. import Socket;
3. import ;
4. import ;
5. import ;
6.
7. public class TCPEchoServerPool {
8.
9. public static void main(String[] args) throws IOException {
版权声明:本文标题:史上最全java socket通信 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1702748419a429216.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论