javajavaweb菜鸟教程,求教两个关于Socket通信的问题

用户名:yjflinchong
文章数:190
评论数:11
访问量:39576
注册日期:
阅读量:1297
阅读量:3317
阅读量:457502
阅读量:1142186
51CTO推荐博文
java socket通信-传输文件图片
ClientTcpSend.java&& 客户端发送类
package com.yjf.
import java.io.DataOutputS
import java.io.F
import java.io.FileInputS
import java.net.InetSocketA
import java.net.S
public class ClientTcpSend {
&& &public static String clientip = &127.0.0.1&;
&& &public static int port = 33456;
&& &public static void main(String[] args) {
&&&&&&& int length = 0;
&&&&&&& byte[] sendBytes =
&&&&&&& Socket socket =
&&&&&&& DataOutputStream dos =
&&&&&&& FileInputStream fis =
&&&&&&& try {
&&&&&&&&&&& try {
&&&&&&&&&&&&&&& socket = new Socket();
&&&&&&&&&&&&&&& socket.connect(new InetSocketAddress(clientip, port),30 * 1000);
&&&&&&&&&&&&&&& dos = new DataOutputStream(socket.getOutputStream());
&&&&&&&&&&&&&&& File file = new File(&F:\\aa.xml&);
&&&&&&&&&&&&&&& fis = new FileInputStream(file);
&&&&&&&&&&&&&&& sendBytes = new byte[1024*4];
&&&&&&&&&&&&&&& while ((length = fis.read(sendBytes, 0, sendBytes.length)) & 0) {
&&&&&&&&&&&&&&&&&&& dos.write(sendBytes, 0, length);
&&&&&&&&&&&&&&&&&&& dos.flush();
&&&&&&&&&&&&&&& }
&&&&&&&&&&& } finally {
&&&&&&&&&&&&&&& if (dos != null)
&&&&&&&&&&&&&&&&&&& dos.close();
&&&&&&&&&&&&&&& if (fis != null)
&&&&&&&&&&&&&&&&&&& fis.close();
&&&&&&&&&&&&&&& if (socket != null)
&&&&&&&&&&&&&&&&&&& socket.close();
&&&&&&&&&&& }
&&&&&&& } catch (Exception e) {
&&&&&&&&&&& e.printStackTrace();
ServerTcpListener.java 服务器监听类
package com.yjf.
import java.net.*;
import java.io.*;
public class ServerTcpListener implements Runnable {
&&& public static void main(String[] args) {
&&&&&&& try {
&&&&&&&&&&& final ServerSocket server = new ServerSocket(ClientTcpSend.port);
&&&&&&&&&&& Thread th = new Thread(new Runnable() {
&&&&&&&&&&&&&&& public void run() {
&&&&&&&&&&&&&&&&&&& while (true) {
&&&&&&&&&&&&&&&&&&&&&&& try {
&&&&&&&&&&&&&&&&&&&&&&&&&&& System.out.println(&开始监听...&);
&&&&&&&&&&&&&&&&&&&&&&&&&&& Socket socket = server.accept();
&&&&&&&&&&&&&&&&&&&&&&&&&&& System.out.println(&有链接&);
&&&&&&&&&&&&&&&&&&&&&&&&&&& receiveFile(socket);
&&&&&&&&&&&&&&&&&&&&&&& } catch (Exception e) {
&&&&&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& }
&&&&&&&&&&& });
&&&&&&&&&&& th.run(); //启动线程运行
&&&&&&& } catch (Exception e) {
&&&&&&&&&&& e.printStackTrace();
&&& public void run() {
&&& public static void receiveFile(Socket socket) {
&&&&&&& byte[] inputByte =
&&&&&&& int length = 0;
&&&&&&& DataInputStream dis =
&&&&&&& FileOutputStream fos =
&&&&&&& try {
&&&&&&&&&&& try {
&&&&&&&&&&&&&&& dis = new DataInputStream(socket.getInputStream());
&&&&&&&&&&&&&&& fos = new FileOutputStream(new File(&E:\\aa.xml&));
&&&&&&&&&&&&&&& inputByte = new byte[1024*4];
&&&&&&&&&&&&&&& System.out.println(&开始接收数据...&);
&&&&&&&&&&&&&&& while ((length = dis.read(inputByte, 0, inputByte.length)) & 0) {
&&&&&&&&&&&&&&&&&&& fos.write(inputByte, 0, length);
&&&&&&&&&&&&&&&&&&& fos.flush();
&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&& System.out.println(&完成接收&);
&&&&&&&&&&& } finally {
&&&&&&&&&&&&&&& if (fos != null)
&&&&&&&&&&&&&&&&&&& fos.close();
&&&&&&&&&&&&&&& if (dis != null)
&&&&&&&&&&&&&&&&&&& dis.close();
&&&&&&&&&&&&&&& if (socket != null)
&&&&&&&&&&&&&&&&&&& socket.close();
&&&&&&&&&&& }
&&&&&&& } catch (Exception e) {
java socket通信-传输文件图片--传输图片java socket通信-传输文件图片--传输图片
来源:http://yijianfengvip./blog/static/0/
本文出自 “” 博客,请务必保留此出处
了这篇文章
类别:未分类┆阅读(0)┆评论(0)Java Socket网络编程,五个常见的原因及解决方法
在Java网络编程中,我们经常性的会碰到一些异常,有些异常是我们反复碰见的,下面整理几条常见的异常,供大家参考交流。
1.java.net.SocketTimeoutException
出现原因:这个异常表示很常见,原因就是Socket超时。
解决方案:一般会有2个地方会抛出这个异常,一个是在Connect的时候,由connect(SocketAddress endpoint,int timeout)中的后者来决定;另外一个就是setSoTimeout(int timeout),这个是设定读取的超时时间。它们设置成0均表示无限大。
2.java.net.BindException:Address already in use: JVM_Bind
出现原因:该异常发生在服务端进行New ServerSocket(Port)或者Socket.bind(bingPort)操作的时候,原因就是与Port一样的一个端口已经被启动,并进行监听。
解决方案:此时呢,我们可以用netstat -an的命令,可以监听到一个Listending状态的端口。只需要找一个没有被占用的端口就能解决问题。或者,我们在使用端口前,优先去查看哪些端口不能使用。(注:Port值为0-65536的整型值)
3.java.net.ConnectException: Connection refused: connect
出现原因:该异常发生在客户端进行new Socket(Ip,Port)或者socket.connect(address,timeout)操作时,原因就是指定的ip地址不能被找到,或者说ip地址存在,但是找不到对应的端口进行监听。
解决方案:首先检查客户端的ip和port是否写错了,假如正确可以测试客户端和服务器端时候可以ping通,如果可以ping通,则在服务端重新找一个可以用的端口;如果ping不通,则需要另外想办法了。
4.java.net.SocketException: Socket is closed
出现原因:该异常在客户端和服务器端均可能发生,原因就是,客户端或者服务器端主动关闭了链接,Spcket的close方法,随后再次对网络链接进行一系列操作。
解决方案:首先我们要弄清楚主动关闭链接的原因,杜绝以后再次被关闭的可能性;然后我们重启客户端和Server端,重新建立通讯即可。
5.java.net.SocketException:Connection reset 或者 Connect reset by peer:Socket write error
出现原因:该异常在客户端和服务器端均可能发生,引发该异常有两个原因:①如果一端的Socket被关闭(主动或者异常引起的关闭)后,另一方还在继续放松数据,发送的第一个数据包机会引发异常Connect reset by peer;②另一个是端退出,但退出时为关闭链接,另一端从连接中读取数据则抛出异常Connection reset.总结一下便是,因为由链接断开后的读和写操作引起的。
解决方案:解决方案如4中的类似,一定要弄清楚一端关闭原因,不要只是简单的重启就解决眼前问题。
每天进步一点点,每天消化一点点。如果文章对你有帮助,点个赞呗。
西安云间科技java培训0学费+0基础+项目实战演练+大咖讲师=高薪就业,150天让你从菜鸟变大咖。
责任编辑:
声明:本文由入驻搜狐号的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
今日搜狐热点2006年8月 Java大版内专家分月排行榜第三
本帖子已过去太久远了,不再提供回复功能。<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
您的访问请求被拒绝 403 Forbidden - ITeye技术社区
您的访问请求被拒绝
亲爱的会员,您的IP地址所在网段被ITeye拒绝服务,这可能是以下两种情况导致:
一、您所在的网段内有网络爬虫大量抓取ITeye网页,为保证其他人流畅的访问ITeye,该网段被ITeye拒绝
二、您通过某个代理服务器访问ITeye网站,该代理服务器被网络爬虫利用,大量抓取ITeye网页
请您点击按钮解除封锁&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)相&#20284;。实际上,由一个程序写入的文件再由另一个程序读取就是一个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协议都定义了一些特殊用途的地址。其中&#20540;得注意的一个是回环地址(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中的多播地址在点分&#26684;式中,第一个数字在224到239之间。IPv6中,多播地址由FF开始。
习惯于通过名字来指代一个主机,例如:。然而,互联网协议只能处理二进制的网络地址,而不是主机名。首先应该明确的是,使用主机名而不使用地址是出于方便性的考虑,这与TCP/IP提供的基本服务是相互独立的。你也可以不使用名字来编写和使用TCP/IP应用程序。当使用名字来定位一个通信终端时,系统将做一些额外的工作把名字解析成地址。有两个原因证明这额外的步骤是&#20540;得的:第一,相对于点分形式(或IPv6中的十六进制数字串),人们更容易记住名字;第二,名字提供了一个间接层,使IP地址的变化对用户不可见。如网络服务器的地址就改变过。由于我们通常都使用网络服务器的名字,而且地址的改变很快就被反应到映射主机名和网络地址的服务上,如从之前的地址208.164.121.48对应到了现在的地址,这种变化对通过名字访问该网络服务器的程序是透明的。名字解析服务可以从各种各样的信息源获取信息。两个主要的信息源是域名系统(DNS,Domain
Name System)和本地配置数据库。DNS是一种分布式数据库。DNS协议允许连接到互联网的主机通过TCP或UDP协议从DNS数据库中获取信息。本地配置数据库通常是一种与具体操作系统相关的机制,用来实现本地名称与互联网地址的映射。
客户端(client)和服务器(server)这两个术语代表了两种角色:客户端是通信的发起者,而服务器程序则被动等待客户端发起通信,并对其作出响应。客户端与服务器组成了应用程序(application)。服务器具有一定的特殊能力,如提供数据库服务,并使任何客户端能够与之通信。一个程序是作为客户端还是服务器,决定了它在与其对等端(peer)建立通信时使用的套接字API的形式(客户端的对等端是服务器,反之亦然)。更进一步来说,客户端与服务器端的区别非常重要,因为客户端首先需要知道服务器的地址和端口号,反之则不需要。如果有必要,服务器可以使用套接字API,从收到的第一个客户端通信消息中获取其地址信息。这与打电话非常相&#20284;:被呼叫者不需要知道拨电话者的电话号码。就像打电话一样,只要通信连接建立成功,服务器和客户端之间就没有区别了。服务器可以使用任何端口号,但客户端必须能够获知这些端口号。在互联网上,一些常用的端口号被约定赋给了某些应用程序。
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()方法。
InetAddressExample.java
运行结果:
% java InetAddressExample
blah.blah 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
blah.blah:
Unable to find address for blah.blah
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 Enumeration&#2;NetworkInterface&#3; getNetworkInterfaces()
static NetworkInterface getByInetAddress(InetAddress addr)
static NetworkInterface getByName(String name)
Enumeration&#2;InetAddress&#3; getInetAddresses()
String getName()
String getDisplayName()
上面第一个方法非常有用,使用它可以很容易获取到运行程序的主机的IP地址:通过getNetworkInterfaces()方法可以获取一个接口列表,再使用实例的getInetAddresses()方法就可以获取每个接口的所有地址。注意:这个列表包含了主机的所有接口,包括不能够向网络中的其他主机发送或接收消息的虚拟回环接口。同样,列表中可能还包括外部不可达的本地链接地址。由于这些列表都是无序的,所以你不能简单地认为,列表中第一个接口的第一个地址一定能够通过互联网访问,而是要通过前面提到的InetAddress类的属性检查方法,来判断一个地址不是回环地址,不是本地链接地址等等。getName()方法返回一个接口(interface)的名字(不是主机名)。这个名字由字母字符串加上一个数字组成,如eth0。在很多系统中,回环地址的名字都是lo0。
客户端向服务器发起连接请求后,就被动地等待服务器的响应。典型的TCP客户端要经过下面三步:
1.创建一个Socket实例:构造器向指定的远程主机和端口建立一个TCP连接。
2. 通过套接字的输入输出流(I/O streams)进行通信:一个Socket连接实例包括一个InputStream和一个OutputStream,它们的用法同于其他Java输入输出流。
3. 使用Socket类的close()方法关闭连接。
TCPEchoClient.java(这是一个通过TCP协议与回馈服务器(echo server)进行通信的客户端)
为什么不只用一个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)
前两个构造函数没有指定本地地址和端口号,因此将采用默认地址和可用的端口号。在有多个接口的主机上指定本地地址是有用的。指定的目的地址字符串参数可以使用与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()方法在输出流上也产生类&#20284;的效果,但在具体实现中,已经写入套接字输出流的数据,将被尽量保证能发送到另一端。注意:默认情况下,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()方法关闭该客户端套接字连接。
TCPEchoServer.java(为我们前面的客户端程序实现了一个回馈服务器。这个服务器程序非常简单,它将一直运行,反复接受连接请求,接收并返回字节信息。直到客户端关闭了连接,它才关闭客户端套接字。)
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协议提供了一种不同于TCP协议的端到端服务。实际上UDP协议只实现两个功能:1)在IP协议的基础上添加了另一层地址(端口),2)对数据传输过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。由于其简单性,UDP套接字具有一些与我们之前所看到的TCP套接字不同的特征。例如,UDP套接字在使用前不需要进行连接。TCP协议与电话通信相&#20284;,而UDP协议则与邮件通信相&#20284;:你寄包裹或信件时不需要进行&连接&,但是你得为每个包裹和信件指定目的地址。类&#20284;的,每条信息(即数据报文,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参数可能比data.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客户端主要执行以下三步:
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. 终止客户端。
UDPEchoClientTimeout.java(UDP版本的回馈客户端,在客户端使用DatagramSocket类的setSoTimeout()方法来指定receive()方法的最长阻塞时间,因此,如果超过了指定时间仍未得到响应,客户端就会重发回馈请求)
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实例,进行通信。
UDPEchoServer.java(UDP版本的回馈服务器。非常简单:它不停地循环,接收数据报文后将相同的数据报文返回给客户端,规定:我们的服务器只接收和发送数据报文中的前255(ECHOMAX)个字符,超出的部分将在套接字的具体实现中无提示地丢弃。)
当在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实例都包含一个内部消息长度&#20540;,而该实例一接收到新消息,这个长度&#20540;都可能改变(以反映实际接收的消息的字节数)。如果一个应用程序使用同一个DatagramPacket实例多次调用receive()方法,每次调用前就必须显式地将消息的内部长度重置为缓存区的实际长度。另一个潜在的问题根源是DatagramPacket类的getData()方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。消息接收到DatagramPacket的缓存区时,只是修改了存放消息数据的地址。在Java1.6中我们可以使用Arrays.copyOfRange()方法,只需要一步就能方便地实现以上功能:byte[]
destBuf = Arrays.copyOfRange(dg.getData(),dg.getOffset(), dg.getOffset()&#43;dg.getLength());
3:发送和接收数据
任何要交换信息的程序之间在信息的编码方式上必须达成共识(如将信息表示为位序列),以及哪个程序发送信息,什么时候和怎样接收信息都将影响程序的行为。程序间达成的这种包含了信息交换的形式和意义的共识称为协议,用来实现特定应用程序的协议叫做应用程序协议,客户端和服务器的行为都要依赖于它们所交换的信息,因此应用程序协议通常更加复杂。
大部分的应用程序协议是根据由字段序列组成的离散信息定义的,其中每个字段中都包含了一段以位序列编码的特定的信息。应用程序协议中明确定义了信息的发送者应该怎样排列和解释这些位序列,同时还要定义接收者应该怎样解析,这样才使信息的接收者能够抽取出每个字段的意义。TCP/IP协议的唯一约束是,信息必须在块(chunks)中发送和接收,而块的长度必须是8位的倍数,因此,我们可以认为在TCP/IP协议中传输的信息是字节序列。鉴于此,我们可以进一步把传输的信息看作数字序列或数组,每个数字的取&#20540;范围是0到255。
OutputStream、InputStream、DatagramPacket实例中所能处理的唯一数据类型是字节和字节数组。作为一种强类型语言,Java需要把其他数据类型(int,String等)显式转换成字节数组。
(1)使用&位操作(bit-diddling)&将消息的正确&#20540;存入字节数组
上面的强制(brute-force)编码方法需要程序员做很多工作:要计算和命名每个数&#20540;的偏移量和大小,并要为编码过程提供合适的参数。如果没有将encodeIntBigEndian()方法提出来作为一个独立的方法,情况会更糟。基于以上原因,强制编码方法是不推荐使用的,而且Java也提供了一些更加易用的内置机制。不过,&#20540;得注意的是强制编码方法也有它的优势,除了能够对标准的Java整型进行编码外,encodeIntegerBigEndian() 方法对1到8字节的任何整数都适用--例如,如果愿意的话,你可以对一个7字节的整数进行编码。
(2)使用Java的内置工具将消息的正确&#20540;存入字节数组
所幸的是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 = ; // onehundred million and one&
static long longVal = 1L;// one trillion and one
ByteArrayOutputStream buf = new ByteArrayOutputStream();&
DataOutputStream out = new DataOutputStream(buf);&
out.writeByte(byteVal);&
out.writeShort(shortVal);&
out.writeInt(intVal);&
out.writeLong(longVal);&
out.flush();&
byte[] msg = buf.toByteArray();
接收方将如何恢复传输的数据呢?正如你想的那样,Java中也提供了与输出工具类相&#20284;的输入工具类,分别是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)所传输的数&#20540;是有符号的(signed)还是无符号的(unsigned)
Java中的四种基本整型都是有符号的,它们的&#20540;以二进制补码(two's-complement)的方式存储,由于Java并不支持无符号整型,如果要在Java中编码和解码无符号数,则需要做一点额外的工作。
字符串和文本
发送者与接收者必须在符号与整数的映射方式上即字符集编码达成共识,,在一组符号与一组整数之间的映射称为编码字符集(coded character set.),Java使用了一种称为Unicode的国际标准编码字符集来表示char型和String型&#20540;。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(socket.getOutputStream()));
在这个例子中,我们先将基本数据的&#20540;,一个一个写入DataOutputStream中,DataOutputStream再将这些数据以二进制的形式写入BufferedOutputStream将三次写入的数据缓存起来,然后再由BufferedOutputStream一次性地将这些数据写入套接字的OutputStream,最后由OutputStream将数据发送到网络。在另一个终端,我们创建了相应的组合InputStream,以有效地接收基本数据类型。
成帧与解析
将数据转换成在线路上传输的&#26684;式只完成了一半工作,在接收端还必须将接收到的字节序列还原成原始信息。应用程序协议通常处理的是由一组字段组成的离散的信息。成帧(Framing)技术则解决了接收端如何定位消息的首尾位置的问题。无论信息是编码成了文本、多字节二进制数、或是两者的结合,应用程序协议必须指定消息的接收者如何确定何时消息已完整接收。
如果一条完整的消息负载在一个DatagramPacket中发送,这个问题就变得很简单了:DatagramPacket 负载的数据有一个确定的长度,接收者能够准确地知道消息的结束位置。然而,如果通过TCP套接字来发送消息,情况将变得更复杂,因为TCP协议中没有消息边界的概念。如果一个消息中的所有字段都有固定的长度,同时每个消息又是由固定数量的字段组成的话,消息的长度就能够确定,接收者就可以简单地将消息长度对应的字节数读到一个byte[]缓存区中。但是如果消息的长度是可变的(例如消息中包含了一些变长的文本字符串),我们事先就无法知道需要读取多少字节。
如果接收者试图从套接字中读取比消息本身更多的字节,将可能发生以下两种情况之一:如果信道中没有其他消息,接收者将阻塞等待,同时无法处理接收到的消息;如果发送者也在等待接收端的响应信息,则会形成死锁(deadlock);另一方面,如果信道中还有其他消息,则接收者会将后面消息的一部分甚至全部读到第一条消息中去,这将产生一些协议错误。因此,在使用TCP套接字时,成帧就是一个非常重要的考虑因素。
一些相同的考虑也适用于查找消息中每个字段的边界:接收者需要知道每个字段的结束位置和下一个字段的开始位置。因此,我们在此介绍的消息成帧技术几乎都可以应用到字段上。然而,最简单并使代码最简洁的方法是将这两个问题分开处理:首先定位消息的结束位置,然后将消息作为一个整体进行解析。
主要有两个技术使接收者能够准确地找到消息的结束位置:
(1)基于定界符(Delimiter-based):消息的结束由一个唯一的标记(unique marker,)指出,即发送者在传输完数据后显式添加的一个特殊字节序列。这个特殊标记不能在传输的数据中出现。
(2)显式长度(Explicit length):在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节。
基于定界符的方法通常用在以文本方式编码的消息中:定义一个特殊的字符或字符串来标识消息的结束。接收者只需要简单地扫描输入信息(以字节的方式)来查找定界序列,并将定界符前面的字符串返回。这种方法的缺点是消息本身不能包含有定界字符,否则接收者将提前认为消息已经结束。在基于定界符的成帧方法中,发送者要保证满足这个先决条件。缺点是发送者和接收者双方都必须扫描消息。
基于长度的方法更简单一些,不过要使用这种方法必须知道消息长度的上限。发送者先要确定消息的长度,将长度信息存入一个整数,作为消息的前缀。消息的长度上限定义了用来编码消息长度所需要的字节数:如果消息的长度小于256字节,则需要1个字节;如果消息的长度小于65536字节,则需要2个字节,等等。
为了展示以上技术,我们将介绍下面定义的Framer接口。它有两个方法:frameMsg()方法用来添加成帧信息并将指定消息输出到指定流,nextMsg()方法则扫描指定的流,从中抽取出下一条消息。
Framer.java
DelimFramer.java类实现了基于定界符的成帧方法,其定界符为&换行&符(&\n&, 字节&#20540;为10)。 frameMethod()方法并没有实现填充,当成帧的字节序列中包含有定界符时,它只是简单地抛出异常。(扩展该方法以实现填充功能:结束符\n,数据中的\n--&ESCy,数据中的ESC--&ESCz,避免数据中碰巧出现ESCy时而被误转化为\n,ESC被称为转义符)nextMsg()方法扫描流,直到读取到了定界符,并返回定界符前面的所有字符,如果流为空则返回null。如果累积了一个消息的不少字符,但直到流结束也没有找到定界符,程序将抛出一个异常来指示成帧错误。
DelimFramer.java
LengthFramer.java类实现了基于长度的成帧方法,适用于长度小于6 ? 1)字节的消息。发送者首先给出指定消息的长度,并将长度信息以big-endian顺序存入两个字节的整数中,再将这两个字节放在完整的消息内容前,连同消息一起写入输出流。在接收端,我们使用DataInputStream以读取整型的长度信息;readFully()
方法将阻塞等待,直到给定的
数组完全填满,这正是我们需要的。&#20540;得注意的是,使用这种成帧方法,发送者不需要检查要成帧的消息内容,而只需要检查消息的长度是否超出了限制。
LengthFramer.java
构建和解析协议消息
简单的&投票&协议,如图所示。一个客户端向服务器发送了一个请求消息,消息中包含了一个候选人ID,范围是0至1000。
程序支持两种请求。一种是查询(inquiry),即向服务器询问给定候选人当前获得的投票总数。服务器发回一个响应消息,包含了原来的候选人ID和该候选人当前(查询请求收到时)获得的选票总数。另一种是投票(voting)请求,即向指定候选人投一票。服务器对这种请求也发回响应消息,包含了候选人ID和其获得的选票数(包括了刚投的一票)。
在实现一个协议时,定义一个专门的类来存放消息中所包含的信息是大有裨益的。该类提供了操作消息中的字段的方法--同时用来维护不同字段之间的不变量。在我们的例子中,客户端和服务器端发送的消息都非常简单,它们唯一的区别是服务器端发送的消息包含了选票总数和一个表示响应消息(不是请求消息)的标志。因此,我们可以用一个类来表示客户端和服务器端的两种消息。
消息类VoteMsg(展示了每条消息中的基本信息)
布尔&#20540;isInquiry,其&#20540;为true时表示该消息是查询请求(为false时表示该消息是投票信息);
布尔&#20540;isResponse,指示该消息是响应(由服务器发送)还是请求;
整型变量candidateID指示了候选人的ID;
长整型变量voteCount指示出所查询的候选人获得的总选票数。
这个类还维护了以下字段间的不变量:
candidateID的范围是0到1000。
voteCount在响应消息中只能是一个非零&#20540;(isResponse为true)。
voteCount 不能为负数。
编码和解码类接口VoteMsgCoder
VoteMsgCoder接口提供了对投票消息进行序列化和反序列化的方法
VoteMsgCoder.java
toWire()方法用于根据一个特定的协议,将投票消息转换成一个字节序列,fromWire()方法则根据相同的协议,对给定的字节序列进行解析,并根据信息的内容构造出消息类的一个实例。
为了介绍不同的信息编码方法,我们展示了两个实现VoteMsgCoder接口的类。一个使用的是基于文本的编码方式,另一个使用的是二进制的编码方式。
基于文本的编码、解码类VoteMsgTextCoder
用文本方式对消息进行编码的版本。该协议指定使用US-ASCII字符集对文本进行编码。消息的开头是一个所谓的&魔术字符串&,即一个字符序列,用于接收
者快速将投票协议的消息和网络中随机到来的垃圾消息区分开。投票/查询布尔&#20540;被编码成字符形式,'v'表示投票消息,'i'表示查询消息。消息的状态,即是否为服务器的响应,由字符'R'指示。状态标记后面是候选人ID,其后跟的是选票总数,它们都编码成十进制字符串。
VoteMsgTextCoder.java
toWire()方法简单地创建一个字符串,该字符串中包含了消息的所有字段,并由空白符隔开。fromWire()方法首先检查&魔术&字符串,如果在消息最前面没有魔术字符串,则抛出一个异常。这里说明了在实现协议时非常重要的一点:永远不要对从网络来的任何输入进行任何假设。你的程序必须时刻为任何可能的输入做好准备,并能够很好地对其进行处理。在这个例子中,如果接收到的不是期望的消息,fromWire()方法将抛出一个异常,否则,就使用Scanner实例,根据空白符一个一个地获取字段。注意,消息的字段数与其是请求消息(由客户端发送)还是响应消息(由服务器发送)有关。如果输入流提前结束或&#26684;式错误,fromWire()方法将抛出一个异常。
基于二进制的编码、解码类VoteMsgBinCoder
与基于文本的&#26684;式相反,二进制&#26684;式使用固定大小的消息。每条消息由一个特殊字节开始,该字节的最高六位为一个&魔术&&#。这一点少量的冗余信息为接收者收到适当的投票消息提供了一定程度的保证。该字节的最低两位对两个布尔&#20540;进行了编码。消息的第二个字节总是0,第三、第四个字节包含了candidateID&#20540;。只有响应消息的最后8个字节才包含了选票总数信息。
服务器中记录投票过程的服务类VoteService
通过流发送消息非常简单,只需要创建消息,调用toWire()方法,添加适当的成帧信息,再写入流。当然,接收消息就要按照相反的顺序执行。这个过程适用于TCP协议,而对于UDP协议,不需要显式地成帧,因为UDP协议中保留了消息的边界信息。为了对发送与接收过程进行展示,我们考虑投票服务的如下几点:1)维护一个候选人ID与其获得选票数的映射,2)记录提交的投票,3)根据其获得的选票数,对查询指定的候选人和为其投票的消息做出响应。首先,我们实现一个投票服务器所用到的服务。当接收到投票消息时,投票服务器将调用VoteService类的handleRequest()
方法对请求进行处理。
TCP投票客户端类VoteClientTCP
该客户端通过TCP套接字连接到投票服务器,在一次投票后发送一个查询请求,并接收查询和投票结果。
发送:消息对象--&编码/解码对象将消息对象编码成字节数组--&成帧/解帧对象将字节数组成帧后通过输出流发送
接收:成帧/解帧对象将接收到的输入流解帧成字节数组--&编码/解码对象将字节数组解码成消息对象--&消息对象
TCP投票服务器端类VoteServerTCP
该服务器反复地接收新的客户端连接,并使用VoteService类为客户端的投票消息作出响应。
UDP投票客户端类VoteClientUDP
UDP版本的投票客户端与TCP版本非常相&#20284;。需要注意的是,在UDP客户端中我们不需要使用成帧器,因为UDP协议为我们维护了消息的边界信息。对于UDP协议,我们使用基于文本的编码方式对消息进行编码,不过只要客户端与服务器能达成一致,也能够很方便地改成其他编码方式。
UDP投票服务器端类VoteServerUDP
UDP投票服务器,同样,也与TCP版本非常相&#20284;。
4:多任务处理
&迭代服务器(iterative server)&:按顺序处理客户端的请求,也就是说在完成了对前一客户端的服务后,才会对下一个客户端进行响应。这种服务器最适用于每个客户端所请求的连接时间都被限制在较小范围内的应用中,而对于允许客户端请求长时间服务的情况,后续客户端将面临无法接受的长时间等待。需要一种方法可以独立处理每一个连接,并使它们不会产生相互干扰,而Java的多线程技术刚好满足了这一需求,这一机制使服务器能够方便地同时处理多个客户端的请求。通过使用多线程,一个应用程序可以并行执行多项任务,就好像有多个Java虚拟机在同时运行。(实际上是多个线程共享了同一个Java虚拟机。)在我们的响应服务器中,可以为每个客户端分配一个执行线程来实现。
两种实现并行服务器(concurrent servers)的编程方法:
(1)一客户一线程(thread-per-client),即为每一个客户端连接创建一个新的线程;&&
(2)线程池(threadpool),即将客户端连接分配给一组事先创建好的线程。
如果客户端的执行过程涉及到需要更新服务器端线程间的共享信息,这将变得相当麻烦。在这种情况下,必须非常小心,以确保不同的线程间在共享数据上得到了妥善的同步,否则,会导致共享信息不一致的状况发生,更麻烦的是这些问题追踪起来还非常困难。
服务器协议类(封装了对每个客户端的处理过程,以回显程序为例)
EchoProtocol中给出了回显协议的代码。这个类的静态方法handleEchoClient()中封装了对每个客户端的处理过程。
一客户一线程
在一客户一线程(thread-per-client)的服务器中,为每个连接都创建了一个新的线程来处理。服务器循环执行一些任务,在指定端口上侦听连接,反复接收客户端传入的连接请求,并为每个连接创建一个新的线程来对其进行处理。
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
与一客户一线程服务器一样,线程池服务器首先创建一个ServerSocket实例。然后创建N个线程,每个线程都反复循环,从(共享的)ServerSocket实例接收客户端连接。当多个线程同时调用同一个ServerSocket实例的accept()方法时,它们都将阻塞等待,直到一个新的连接成功建立。然后系统选择一个线程,新建立的连接对应的Socket实例则只在选中的线程中返回。其他线程则继续阻塞,直到成功建立下一个连接和选中另一个幸运的线程。由于线程池中的所有线程都反复循环,一个接一个地处理客户端连接,线程池服务器的行为就像是一组迭代服务器。与一客户一线程服务器不同,线程池中的线程在完成对一个客户端的服务后并不终止,相反,它又重新开始在accept()方法上阻塞等待。
由于线程的重复使用,线程池的方法只需要付出创建N次线程的系统开销,而与客户端连接总数无关。由于可以控制最大并发执行线程数,我们就可以控制线程的调度和资源开
销。当然,如果我们创建的线程太少,客户端还是有可能等很长时间才获得服务,因此,线程池的大小需要根据负载情况进行调整,以使客户端连接的时间最短。理想的情况是有一个调度工具,可以在系统负载增加时扩展线程池的大小(低于大小上限),负载较轻时缩减线程池的大小。
利用JDK 提供的线程池(java.util.concurrent包中)来实现并行服务器
Executors类的newCachedThreadPool()静态工厂方法创建了一个ExecutorService实例。在使用一个实现了Runnable接口的实例调用它的execute()方法时,如果必要它将创建一个新的线程来处理任务。然而,它首先会尝试使用已有的线程。如果一个线程空闲了60秒以上,则将移出线程池。这个策略几乎总是比前面两个TCPEchoServer*例子的效率高。service的execute()方法,该方法要么将其分配给一个已有的线程,要么创建一个新的线程来处理它。&#20540;得注意的是,当达到稳定状态时,缓存线程池服务最终将保持合适的线程数,以使每个线程都保持忙碌,同时又很少创建或销毁线程。
阻塞和超时
Socket的I/O调用可能会因为多种原因而阻塞。数据输入方法read()和receive()在没有数据可读时会阻塞。TCP套接字的write()方法在没有足够的空间缓存传输的数据时可能阻塞。 ServerSocket的accept()方法和Socket的构造函数都会阻塞等待,直到连接建立。同时,长的信息往返时间,高错误率的连接和慢速的(或已发生故障的)服务器,都可能导致需要很长的时间来建立连接。NIO包中的更加强大的非阻塞工具。
对于read()、accept()和receive()的阻塞,使用Socket类、ServerSocket类和DatagramSocket类的setSoTimeout()方法,设置其阻塞的最长时间(以毫秒为单位)。如果在指定时间内这些方法没有返回,则将抛出一个InterruptedIOException异常。对于Socket实例,在调用read()方法前,我们还可以使用该套接字的InputStream的available()方法来检测是否有可读的数据。
对于Socket连接服务器的阻塞(Socket类的构造函数会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时)不幸的是,系统定义的超时时间很长,而Java又没有提供任何缩短它的方法。要改变这种情况,可以使用Socket类的无参数构造函数,它返回的是一个没有建立连接的Socket实例。需要建立连接时,调用该实例的connect()方法,并指定一个远程终端和超时时间(毫秒)。
对于write方法阻塞(write()方法调用也会阻塞等待,直到最后一个字节成功写入到了TCP实现的本地缓存中)如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端。因此,write()方法的阻塞总时间最终还是取决于接收端的应用程序。不幸的是Java现在还没有提供任何使write()超时或由其他线程将其打断的方法。所以如果一个可以在Socket实例上发送大量数据的协议可能会无限期地阻塞下去。
& & & &限制每个客户端的时间,实现一个为每个客户端限定了服务时间的回显协议。也就是说我们定义一个了目标,TIMELIMIT,并在协议中实现经过TIMELIMIT毫秒后,实例就自动终止。协议实例保持了对剩余服务时间的跟踪,并使用setSoTimeout()方法来保证read()方法的阻塞时间不会超过TIMELIMIT。由于没有办法限制write()调用的时间,我们并不能保证所定义的时间限制真正有效。尽管如此,TimelimitEchoProtocol.java还是实现了这种方法;要与TCPEchoServerExecutor.java一起使用,只需要简单地将while循环的第二行改为:service.execute(new
TimeLimitEchoProtocol(clntSock, logger));
TimelimitEchoProtocol类与EchoProtocol类非常相&#20284;,唯一的区别在于它试图将回显连接的总服务时间限制在10秒钟之内。当handleEchoClient() 方法被调用时,就通过当前时间和服务期限计算出了服务的截止时间。每次read()调用结束后将重新计算当前时间与截止时间的差&#20540;,即剩余服务时间,并将套接字超时设置为该剩余时间。
一个服务器和一个客户端。这种一对一的通信方法有时称为单播(unicast),有两种类型的一对多(one-to-many)服务:广播(broadcast)和多播(multicast)。对于广播,(本地)网络中的所有主机都会接收到一份数据副本。对于多播,消息只是发送给一个多播地址(multicast address),网络只是将数据分发给那些表示想要接收发送到该多播地址的数据的主机。总的来说,只有UDP套接字允许广播或多播。
广播UDP数据报文与单播数据报文相&#20284;,唯一的区别是其使用的是一个广播地址而不是一个常规的(单播)IP地址。注意,IPv6并没有明确地提供广播地址;然而,有一个特殊的全节点(all - nodes)、本地连接范围(link-local-scope)的多播地址,FFO2::1,发送给该地址的消息将多播到一个连接上的所有节点。IPv4的本地广播地址(255.255.255.255)将消息发送到在同一广播网络上的每个主机。本地广播信息决不会被路由器转发。在以太网上的一个主机可以向在同一以太网内的其他主机发送消息,但是该消息不会被路由器转发。IPv4还指定了定向广播地址,允许向指定网络中的所有主机进行广播;然而,互联网上的大部分路由器都不转发定向广播消息。
并不存在可以向网络范围内所有主机发送消息的广播地址。至于为什么没有,请考虑向互联网上每台主机发送广播消息可能产生的影响。在这种地址发送单个数据报文就可能会由路由器产生非常大量的数据包副本,并可能会耗尽所有网络的带宽。误用(恶意的或意外的)该地址的后果会非常严重,因此IP协议的设计者故意没有定义互联网范围的广播机制。在Java中,单播和广播的代码是相同的。要实现具有广播功能的应用程序,我们可以简单地在VoteClientUDP.java中使用广播目的地址。
与广播一样,多播与单播之间的一个主要区别是地址的形式。一个多播地址指示了一组接收者。IP协议的设计者为多播分配了一定范围的地址空间,IPv4中的多播地址范围是224.0.0.0到239.255.255.255,IPv6中的多播地址是任何由FF开头的地址。除了少数系统保留的多播地址外,发送者可以向以上范围内的任何地址发送数据。Java中多播应用程序主要通过MulticastSocke实例进行通信,它是DatagramSocket的一个子类。重点需要理解的是,一个MulticastSocket实例实际上就是一个UDP套接字(DatagramSocket),其包含了一些额外的可以控制的多播特定属性。
多播发送者:
与广播不同,网络多播只将消息副本发送给指定的一组接收者。这组接收者叫做多播组(multicast group),通过共享的多播(组)地址确定。接收者需要一种机制来通知网络它对发送到某一特定地址的消息感兴趣,以使网络将数据包转发给它。这种通知机制叫做加入一组(joining a group),可以由MulticastSocket类的joinGroup()方法实现。我们的多播接收者加入了一个特定的组,接收并打印该组的一条多播消息,然后退出。
多播接收者:
多播和单播接收者唯一的重要区别是,多播接收者表明希望从哪个多播地址接收数据来加入多播组。不过MulticastSocket还有一些DatagramSocket没有的能力,包括1)允许指定数据报文的TTL,和2)允许指定和改变通过哪个接口将数据报文发送到组(接口由其互联网地址确定)。
决定使用广播还是使用多播需要考虑多方面的因素,包括接收者的网络地址和通信双方的知识。互联网广播的范围是限定在一个本地广播网络之内的,并对广播接收者的位置进行了严&#26684;的限制。多播通信可能包含网络中任何位置的接收者,[ ]因此多播有个好处就是它能够覆盖一组分布在各处的接收者。IP多播的不足在于接收者必须知道要加入的多播组的地址。而接收广播信息则不需要指定地址信息。在某些情况下,广播是一个比多播更好更易于发现的机制。所有主机在默认情况下都可以接收广播
Keep-Alive机制
如果一段时间内没有数据交换,通信的每个终端可能都会怀疑对方是否还处于活跃状态。TCP协议提供了一种keep-alive的机制,该机制在经过一段不活动时间后,将向另一个终端发送一个探测消息。如果另一个终端还出于活跃状态,它将回复一个确认消息。如果经过几次尝试后依然没有收到另一终端的确认消息,则终止发送探测信息,关闭套接字,并在下一次尝试I/O操作时抛出一个异常。注意,应用程序只要在探测信息失败时才能察觉到keep-alive机制的工作。
boolean getKeepAlive()&
void setKeepAlive(boolean on)
默认情况下,keep-alive机制是关闭的。通过调用setKeepAlive()方法将其设置为true来开启keep-alive机制。
发送和接收缓存区的大小
一旦创建了一个Socket或DatagramSocket实例,操作系统就必须为其分配缓存区以存放接收的和要发送的数据。Socket, DatagramSocket: 设置和获取发送接收缓存区大小(以字节为单位)
int getReceiveBufferSize()&
void setReceiveBufferSize(int size)&
int getSendBufferSize()
&void setSendBufferSize(int size)
还可以在ServerSocket上指定接收缓冲区大小。不过,这实际上是为accept()方法所创建的新Socket实例设置接收缓冲区大小。为什么可以只设置接收缓冲区大小而不设置发送缓冲区的大小呢?当接收了一个新的Socket,它就可以立刻开始接收数据,因此需要在accept()方法完成连接之前设置好缓冲区的大小。另一方面,由于可以控制什么时候在新接受的套接字上发送数据,因此在发送之前还有时间设置发送缓冲区的大小。
ServerSocket: 设置/获取所接受套接字的接收缓冲区大小
int getReceiveBufferSize()&
void setReceiveBufferSize(int size)
消除缓冲延迟
TCP协议将数据缓存起来直到足够多时一次发送,以避免发送过小的数据包而浪费网络资源。虽然这个功能有利于网络,但应用程序可能对所造成的缓冲延迟不能容忍。好在可以人为禁用缓存功能。
Socket: 设置/获取TCP缓冲延迟
boolean getTcpNoDelay()&
void setTcpNoDelay(boolean on)
getTcpNoDelay()和setTcpNoDelay()方法用于获取和设置是否消除缓冲延迟。将&#20540;设置为true表示禁用缓冲延迟功能。
调用Socket的close()方法将同时终止两个方向(输入和输出)的数据流。一旦一个终端(客户端或服务器端)关闭了套接字,它将无法再发送或接收数据。这就意味着close()方法只能在调用者完成通信之后用来给另一端发送信号。
Socket类的shutdownInput()和shutdownOutput()方法能够将输入输出流相互独立地关闭。调用shutdownInput()后,套接字的输入流将无法使用。任何没有发送的数据都将毫无提示地被丢弃,任何想从套接字的输入流读取数据的操作都将返回-1。当Socket调用shutdownOutput() 方法后,套接字的输出流将无法再发送数据,任何尝试向输出流写数据的操作都将抛出一个IOException异常。在调用shutdownOutput()之前写出的数据可能能够被远程套接字读取,之后,在远程套接字输入流上的读操作将返回-1。应用程序调用shutdownOutput()后还能继续从套接字读取数据,类&#20284;的,在调用shutdownInput()后也能够继续写数据。
NIO利用操作Buffer的信道Channel(轮询的目标)的非阻塞特性和轮询I/O状态的选择器Selector(一次轮询一组客户端)就可以在单线程下为任意数量的连接提供服务。
NIO主要包括两个部分:java.nio.channels包介绍了Selector和Channel抽象,java.nio包介绍了Buffer抽象
为什么需要NIO?
(1)::由于创建、维护和切换线程需要的系统开销,一客户一线程方式在系统扩展性方面受到了限制。使用线程池可以节省那种系统开销,同时允许实现者利用并行硬件的优势。但对于连接生存期比较长的协议来说,线程池的大小仍然限制了系统可以同时处理的客户端数量。
(2):在使用线程的扩展性方面还涉及一些更加难以把握的挑战。其中一个挑战就是程序员几乎不能对什么时候哪个线程将获得服务进行控制。你可以设置一个线程实例的优先级(priority)(高优先级的线程相对于低优先级的线程有优先权),但是这个优先级只是一种&建议&--下一个选择执行的线程完全取决于具体实现
(3):所有客户之间共享一些状态信息(即调度表)需要通过使用锁(locks)机制或其他互斥机制对依次访问状态进行严&#26684;的同步(synchronized),对共享状态进行同步访问,要同时考虑到多线程服务器的正确性和高效性就变得非常困难,同时使用同步机制将增加更多的系统调度和上下文切换开销,而程序员对这些开销又无法控制。
一个Channel实例代表了一个&可轮询的(pollable)&I/O目标,如套接字(或一个文件、设备等)。Channel能够注册一个Selector类的实例。Selector的select()方法允许你询问&在一组信道中,哪一个当前需要服务(即,被接受,读或写)?&。
Buffer抽象代表了一个有限容量(finite-capacity)的数据容器--其本质是一个数组,由指针指示了在哪存放数据和从哪读取数据。Buffer则提供了比Stream抽象更高效和可预测的I/O。 Stream抽象好的方面是隐藏了底层缓冲区的有限性,提供了一个能够容纳任意长度数据的容器的假象。坏的方面是要实现这样一个假象,要么会产生大量的内存开销,要么会引入大量的上下文切换,甚至可能两者都有。在使用线程时,这些开销都隐藏在了具体实现中,因此也失去了对其的可控性和可预测性。这种方法使编写程序变得容易,但要调整它们的性能则变得更困难。不幸的是,如果要使用Java的Socket抽象,流就是唯一的选择
使用Buffer有两个主要好处。第一,与读写缓冲区数据相关联的系统开销暴露给了程序员。例如,如果想要向缓冲区存入数据,但又没有足够的空间时,就必须采取一些措施来获得空间(即,移出一些数据,或移开已经在那个位置的数据来获得空间,或者创建一个新的实例)。这意味着需要额外的工作,但是你(程序员)可以控制它什么时候发生,如何发生,以及是否发生。一个聪明的程序员如果清楚地了解了应用程序的需求,就那能通过权衡这些选择来降低系统开销。第二,一些对Java对象的特殊Buffer映射操作能够直接操作底层平台的资源(例如,操作系统的缓冲区)。这些操作节省了在不同地址空间中复制数据的开销--这在现代计算机体系结构中是开销很大的操作。
Channel实例代表了一个与设备的连接,通过它可以进行输入输出操作。实际上Channel的基本思想与我们见过的普通套接字非常相&#20284;。对于TCP协议,可以使用ServerSocketChannel和SocketChannel。还有一些针对其他设备的其他类型信道(如,FileChannel),信道(channel)和套接字(socket)之间的不同点之一,可能是信道通常要调用静态工厂方法来获取实例:
SocketChannel clntChan = SocketChannel.open();
&ServerSocketChannel servChan = ServerSocketChannel.open();
Channel使用的不是流,而是缓冲区来发送或读取数据。Buffer类或其任何子类的实例都可以看作是一个定长的Java基本数据类型元素序列。与流不同,缓冲区有固定的、有限的容量,并由内部(但可以被访问)状态记录了有多少数据放入或取出,就像是有限容量的队列一样。Buffer是一个抽象类,只能通过创建它的子类来获得Buffer实例,而每个子类都设计为用来容纳一种Java基本数据类型(boolean除外)。因此,这些实例分别为FloatBuffer,或IntBuffer,或ByteBuffer,等等(ByteBuffer是这些实例中最灵活的)。在channel中使用Buffer实例通常不是使用构造函数创建的,而是通过调用allocate()方法创建指定容量的Buffer实例,
ByteBuffer buffer = ByteBuffer.allocate(CAPACITY);
或通过包装一个已有的数组来创建:
ByteBuffer buffer = ByteBuffer.wrap(byteArray);
套接字的某些操作可能会无限期地阻塞,如创建/接收连接或读写数据等I/O调用,都可能无限期地阻塞等待,直到底层的网络实现发生了什么。慢速的、有损耗的网络,或仅仅是简单的网络故障都可能导致任意时间的延迟。然而不幸的是,在调用一个方法之前无法知道其是否会阻塞。而NIO的强大功能部分来自于channel的非阻塞特性。NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。
clntChan.configureBlocking(false);
在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回&#20540;指示了所请求的操作完成的程度。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求在等待,则返回客户端SocketChannel,否则返回null。
由于该套接字是非阻塞式的,因此对connect()方法的调用可能会在连接建立之前返回,如果在返回前已经成功建立了连接,则返回true,否则返回false。对于后一种情况,任何试图发送或接收数据的操作都将抛出NotYetConnectedException异常,因此,我们通过持续调用finishConnect()方法来&轮询&连接状态,该方法在连接成功建立之前一直返回false。打印操作演示了在等待连接建立的过程中,程序还可以执行其他任务。不过,这种忙等的方法非常浪费系统资源,这里这样做只是为了演示该方法的使用。
只有非阻塞信道才可以注册选择器,因此需要将其配置为适当的状态。由于select()操作只是向Selector所关联的键集合中添加元素,因此,如果不移除每个处理过的键,它就会在下次调用select()方法是仍然保留在集合中,而且可能会有无用的操作来调用它。TCPServerSelector的大部分内容都与协议无关,只有协议赋&#20540;那一行代码是针对的特定协议。所有协议细节都包含在了TCPProtocol接口的具体实现中。EchoSelectorProtocol类就实现了该回显协议的操作器。
Buffer详解
NIO中,数据的读写操作始终是与缓冲区相关联的。Channel将数据读入缓冲区,然后我们又从缓冲区访问数据。写数据时,首先将要发送的数据按顺序填入缓冲区。基本上,缓冲区只是一个列表,它的所有元素都是基本数据类型(通常为字节型)。缓冲区是定长的,它不像一些类那样可以扩展容量(例如,List,StringBuffer等)。注意,ByteBuffer是最常用的缓冲区,因为:1)它提供了读写其他数据类型的方法,2)信道的读写方法只接收ByteBuffer。
Buffer索引:
缓冲区不仅仅是用来存放一组元素的列表。在读写数据时,它有内部状态来跟踪缓冲区的当前位置,以及有效可读数据的结束位置等,为了实现这些功能,每个缓冲区维护了指向其元素列表的4个索引
position和limit之间的距离指示了可读取/存入的字节数。Java中提供了两个方便的方法来计算这个距离。
ByteBuffer: 剩余字节
boolean hasRemaining()
&int remaining()
当缓冲区至少还有一个元素时,hasRemaining()方法返回true,remaining()方法返回剩余元素的个数。
在这些变量中,以下关系保持不变:
0 ≤ mark ≤ position ≤ limit ≤ capacity
mark变量的&#20540;&记录&了一个将来可返回的位置,reset()方法则将position的&#20540;还原成上次调用mark()方法后的position&#20540;(除非这样做会违背上述的不变关系)。
创建Buffer:
使用分配空间的方式来创建缓冲区其实与使用包装的方法区别不大。惟一的区别是allocate()方法创建了自己的后援数组。在缓冲区上调用array()方法即可获得后援数组的引用。
通过包装的方法创建的缓冲区保留了被包装数组内保存的数据。实际上,wrap()方法只是简单地创建了一个具有指向被包装数组的引用的缓冲区,该数组称为后援数组。对后援数组中的数据做的任何修改都将改变缓冲区中的数据,反之亦然。如果我们为wrap()方法指定了偏移量(offse

我要回帖

更多关于 javaweb菜鸟教程 的文章

 

随机推荐