自己整理的socket教程

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

⾃⼰整理的socket教程⼀.教程提⽰ (1)
1.我应该学习本教程吗 (1)
2.获取帮助 (1)
⼆.套接字基础 (2)
1.介绍 (2)
2.计算机组⽹101 (2)
3.套接字位于什么地⽅ (2)
4. 把套接字暴露给应⽤程序 (3)
5.什么是套接字 (4)
6.套接字的类型 (4)
三.⼀个神秘的套接字 (6)
1.介绍 (6)
2.甚⾄不⽤尝试就可以使⽤套接字 (6) 3.URLClient 类 (6)
4.浏览⽂档 (7)
5.从服务器请求⼀个⽂档 (7)
6.总结 (8)
四.⼀个简单的⽰例 (9)
1.背景 (9)
2.创建RemoteFileClient类 (9)
3.实现main() (10)
4.建⽴连接 (10)
5.与主机交谈 (11)
6.断开连接 (11)
7.总结⼀下客户机 (12)
8.创建RemoteFileServer类 (12)
9.实现mian() (12)
10.接受连接 (13)
11.处理连接 (13)
12.总结⼀下服务器 (14)
五.⼀个多线程的⽰例 (15)
1.介绍 (15)
2.接受(太多)连接 (15)
3.处理连接:第⼀部分 (15)
4.处理连接:第⼆部分 (16)
5.实现run() (16)
6.总结⼀下多线程服务器 (17)
六.⼀个带有连接池的⽰例 (18)
1.介绍 (18)
2.创建PooledRemoteFileServer (18)
3.实现main() (19)
4.建⽴连接处理程序 (19)
5.处理连接 (19)
6.填充连接池 (20)
7.从池中获取连接 (21)
8.处理连接:再⼀次 (22)
1.介绍 (23)
2.客户机端 (23)
3.服务器端 (24)
4.业务逻辑 (24)
5.发送消息到服务器 (24)
6.接受来⾃服务器的消息 (25)
⼋.总结 (26)
1.总结 (26)
2.参考资料 (26)
九.附录 (27)
1.URLClient的代码清单 (27)
2.RemoteFileClient的代码清单 (27)
3.RemoteFileServer的代码清单 (29)
4.MultithreadedRemoteFileServer的代码清单 (30)
5.ConnectionHandler的代码清单 (31)
6.PooledRemoteFileServer的代码清单 (31)
7.PooledConnectionHandler的代码清单 (32)
⼀.教程提⽰
1.我应该学习本教程吗
套接字(socket)为两台计算机之间的通信提供了⼀种机制,在James Gosling 注意到Java 语⾔之前,套接字就早已赫赫有名。

该语⾔只是让您不必了解底层操作系统的细节就能有效地使⽤套接字。

多数着重讨论Java 编码的书或者未涵盖这个主题,或者给读者留下很⼤的想象空间。

本教程将告诉您开始在代码中有效地使⽤套接字时,您真正需要知道哪些知识。

我们将专门讨论以下问题:
什么是套接字
它位于您可能要写的程序的什么地⽅
能⼯作的最简单的套接字实现—以帮助您理解基础知识
详细剖析另外两个探讨如何在多线程和具有连接池环境中使⽤套接字的⽰例
简要讨论⼀个现实世界中的套接字应⽤程序
如果您能够描述如何使⽤ 包中的类,那么本教程对您来说也许基础了点,虽然⽤它来提⾼⼀下还是不错的。

如果您在PC 和其它平台上使⽤套接字已经⼏年,那么最初的部分也许会使您觉得烦。

但如果您不熟悉套接字,⽽且只是想知道什么是套接字以及如何在Java 代码中有效地使⽤它们,那么本教程就是⼀个开始的好地⽅。

2.获取帮助
有关于本教程内容⽅⾯的问题,请与作者Roy Miller(在*************************)或Adam Williams(在
***************************)联系。

Roy Miller 和Adam Williams 都是RoleModel Software, Inc. 的软件开发者。

他们在Dallas Semiconductor 的TINI Java 平台上共同开发了⼀个基于套接字的应⽤程序原型。

Roy 和Adam ⽬前正在使⽤套接字把COBOL ⾦融交易系统移植到该Java 平台上。

加盟RoleModel 之前,Roy 在Andersen Consulting(现为Accenture)做了六年的软件开发和项⽬管理。

他与别⼈合著的Extreme Programming Applied: Playing to Win(Addison-Wesley XP 系列)将于2001 年10 ⽉出版。

⼆.套接字基础
1.介绍
多数程序员,不管他们是否使⽤Java 语⾔进⾏编码,都不想很多知道关于不同计算机上的应⽤程序彼此间如何通信的低级细节。

程序员们希望处理更容易理解的更⾼级抽象。

Java 程序员希望能⽤他们熟悉的Java 构造,通过直观接⼝与对象交互。

套接字在两个领域中都存在—我们宁愿避开的低级细节和我们更愿处理的抽象层。

本教程讨论的低级细节将只限于理解抽象应⽤程序所必须的部分。

2.计算机组⽹101
计算机以⼀种⾮常简单的⽅式进⾏相互间的操作和通信。

计算机芯⽚是以 1 和0 的形式存储并传输数据的开—闭转换器的集合。

当计算机想共享数据时,它们所需做的全部就是以⼀致的速度、顺序、定时等等来回传输⼏百万⽐特和字节的数据流。

每次想在两个应⽤程序之间进⾏信息通信时,您怎么会愿意担⼼那些细节呢?
为免除这些担⼼,我们需要每次都以相同⽅式完成该项⼯作的⼀组包协议。

这将允许我们处理应⽤程序级的⼯作,⽽不必担⼼低级⽹络细节。

这些成包协议称为协议栈(stack)。

TCP/IP 是当今最常见的协议栈。

多数协议栈(包括TCP/IP)都⼤致对应于国际标准化组织(International Standards Organization,ISO)的开放系统互连参考模型(Open Systems Interconnect Reference Model,OSIRM)。

OSIRM 认为在⼀个可靠的计算机组⽹中有七个逻辑层(见图)。

各个地⽅的公司都对这个模型某些层的实现做了⼀些贡献,从⽣成电⼦信号(光脉冲、射频等等)到提供数据给应⽤程序。

TCP/IP 映射到OSI 模型中的两层的情形如图所⽰。

我们不想涉及层的太多细节,但您应该知道套接字位于什么地⽅。

3.套接字位于什么地⽅
套接字⼤致驻留在OSI 模型的会话层(见图)。

会话层夹在其上⾯向应⽤的层和其下的实时数据通信层之间。

会话层为两台计算机之间的数据流提供管理和控制服务。

作为该层的⼀部分,套接字提供⼀个隐藏从导线上获取⽐特和字节的复杂性的抽象。

换句话说,套接字允许我们让应⽤程序表明它想发送⼀些
当您打电话时,您的声⾳传到传感器,传感器把它转换成可以传输的电数据。

电话机是⼈与电信⽹络的接⼝。

您⽆须知道声⾳如何传输的细节,只要知道想打电话给谁就⾏了。

同样地,套接字扮演隐藏在未知通道上传输1 和0 的复杂性的⾼级接⼝的⾓⾊。

4. 把套接字暴露给应⽤程序
使⽤套接字的代码⼯作于表⽰层。

表⽰层提供应⽤层能够使⽤的信息的公共表⽰。

假设您打算把应⽤程序连接到只能识别EBCDIC 的旧的银⾏系统。

应⽤程序的域对象以ASCII 格式存储信息。

在这种情况下,您得负责在表⽰层上编写把数据从EBCDIC 转换成ASCII 的代码,然后(⽐⽅说)给应⽤层提供域对象。

应⽤层然后就可以⽤域对象来做它想做的任何事情。

您编写的套接字处理代码只存在于表⽰层中。

您的应⽤层⽆须知道套接字如何⼯作的任何事情。

5.什么是套接字
既然我们已经知道套接字扮演的⾓⾊,那么剩下的问题是:什么是套接字?Bruce Eckel 在他的《Java 编程思想》⼀书中这样描述套接字:
套接字是⼀种软件抽象,⽤于表达两台机器之间的连接“终端”。

对于⼀个给定的连接,每台机器上都有⼀个套接字,您也可以想象它们之间有⼀条虚拟的“电缆”,“电缆”的每⼀端都插⼊到套接字中。

当然,机器之间的物理硬件和电缆连接都是完全未知的。

抽象的全部⽬的是使我们⽆须知道不必知道的细节。

简⾔之,⼀台机器上的套接字与另⼀台机器上的套接字交谈就创建⼀条通信通道。

程序员可以⽤该通道来在两台机器之间发送数据。

当您发送数据时,TCP/IP 协议栈的每⼀层都会添加适当的报头信息来包装数据。

这些报头帮助协议栈把您的数据送到⽬的地。

好消息是Java 语⾔通过"流"为您的代码提供数据,从⽽隐藏了所有这些细节,这也是为什么它们有时候被叫做流套接字(streaming socket)的原因。

把套接字想成两端电话上的听筒—我和您通过专⽤通道在我们的电话听筒上讲话和聆听。

直到我们决定挂断电话,对话才会结束(除⾮我们在使⽤蜂窝电话)。

⽽且我们各⾃的电话线路都占线,直到我们挂断电话。

如果想在没有更⾼级机制如ORB(以及CORBA、RMI、IIOP 等等)开销的情况下进⾏两台计算机之间的通信,那么套接字就适合您。

套接字的低级细节相当棘⼿。

幸运的是,Java 平台给了您⼀些虽然简单但却强⼤的更⾼级抽象,使您可以容易地创建和使⽤套接字。

6.套接字的类型
⼀般⽽⾔,Java 语⾔中的套接字有以下两种形式:
TCP 套接字(由Socket 类实现,稍后我们将讨论这个类)
UDP 套接字(由DatagramSocket 类实现)
TCP 和UDP 扮演相同⾓⾊,但做法不同。

两者都接收传输协议数据包并将其内容向前传送到表⽰层。

TCP 把消息分解成数据包(数据报,datagrams)并在接收端以正确的顺序把它们重新装配起来。

TCP 还处理对遗失数据包的重传请求。

有了TCP,位于上层的层要担⼼的事情就少多了。

UDP 不提供装配和重传请求这些功能。

它只是向前传送信息包。

位于上层的层必须确保消息是完整的并且是以正确的顺序装配的。

⼀般⽽⾔,UDP 强加给您的应⽤程序的性能开销更⼩,但只在应⽤程序不会突然交换⼤量数据并且不必装配⼤量数据报以完成⼀条消息的时候。

否则,TCP 才是最简单或许也是最⾼效的选择。

因为多数读者都喜欢TCP 胜过UDP,所以我们将把讨论限制在Java 语⾔中⾯向TCP 的类。

三.⼀个神秘的套接字
1.介绍
Java 平台在 包中提供套接字的实现。

在本教程中,我们将与 中的以下三个类⼀起⼯作:URLConnection
Socket
ServerSocket
中还有更多的类,但这些是您将最经常碰到的。

让我们从URLConnection开始。

这个类为您不必了解任何底层套接字细节就能在Java 代码中使⽤套接字提供⼀种途径。

2.甚⾄不⽤尝试就可以使⽤套接字
URLConnection 类是所有在应⽤程序和URL 之间创建通信链路的类的抽象超类。

URLConnection 在获取Web 服务器上的⽂档⽅⾯特别有⽤,但也可⽤于连接由URL 标识的任何资源。

该类的实例既可⽤于从资源中读,也可⽤于往资源中写。

例如,您可以连接到⼀个servlet 并发送⼀个格式良好的XML String 到服务器上进⾏处理。

URLConnection 的具体⼦类(例如HttpURLConnection)提供特定于它们实现的额外功能。

对于我们的⽰例,我们不想做任何特别的事情,所以我们将使⽤URLConnection 本⾝提供的缺省⾏为。

连接到URL 包括⼏个步骤:
创建URLConnection
⽤各种setter ⽅法配置它
连接到URL
⽤各种getter ⽅法与它交互
接着,我们将看⼀些演⽰如何⽤URLConnection 来从服务器请求⽂档的样本代码。

3.URLClient 类
我们将从URLClient 类的结构讲起。

import java.io.*;
import .*;
public class URLClient {
protected URLConnection connection;
public static void main(String[] args) {
}
public String getDocumentAt(String urlString) {
}
}
要做的第⼀件事是导⼊ 和java.io。

我们给我们的类⼀个实例变量以保存⼀个URLConnection。

我们的类有⼀个main() ⽅法,它处理浏览⽂档的逻辑流。

我们的类还有⼀个getDocumentAt() ⽅法,
4.浏览⽂档
main()⽅法处理浏览⽂档的逻辑流:
public static void main(String[] args) {
URLClient client = new URLClient();
String yahoo = client.getDocumentAt("");
System.out.println(yahoo);
}
我们的main() ⽅法只是创建⼀个新的URLClient 并⽤⼀个有效的URL String 调⽤getDocumentAt()。

当调⽤返回该⽂档时,我们把它存储在String,然后将它打印到控制台。

然⽽,实际的⼯作是在getDocumentAt() ⽅法中完成的。

5.从服务器请求⼀个⽂档
getDocumentAt() ⽅法处理获取Web 上的⽂档的实际⼯作:
public String getDocumentAt(String urlString) {
StringBuffer document = new StringBuffer();
try {
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null)
document.append(line + "\n");
reader.close();
} catch (MalformedURLException e) {
System.out.println("Unable to connect to URL: " + urlString);
} catch (IOException e) {
System.out.println("IOException when connecting to URL: " + urlString);
}
return document.toString();
}
getDocumentAt()⽅法有⼀个String参数,该参数包含我们想获取的⽂档的 URL。

我们在开始时创建⼀个StringBuffer来保存⽂档的⾏。

然后我们⽤我们传进去的urlString创建⼀个新URL。

接着创建⼀个URLConnection并打开它:
URLConnection conn = url.openConnection();
⼀旦有了⼀个URLConnection,我们就获取它的InputStream并包装进InputStreamReader,然后我们⼜把InputStreamReader 包装进BufferedReader以使我们能够读取想从服务器上获取的⽂档的⾏。

在Java 代码中处理套接字时,我们将经常使⽤这种包装技术,但我们不会总是详细讨论它。

在我们继续往前讲之前,您应该熟悉它:
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
有了BufferedReader,就使得我们能够容易地读取⽂档内容。

我们在while循环中调⽤reader上的
String line = null;
while ((line = reader.readLine()) != null)
document.append(line + "\n");
对readLine() 的调⽤将直⾄碰到⼀个从InputStream 传⼊的⾏终⽌符(例如换⾏符)时才阻塞。

如果没碰到,它将继续等待。

只有当连接被关闭时,它才会返回null。

在这个案例中,⼀旦我们获取⼀个⾏(line),我们就把它连同⼀个换⾏符⼀起附加(append)到名为document 的StringBuffer 上。

这保留了服务器端上读取的⽂档的格式。

我们在读完⾏之后关闭BufferedReader:
reader.close();
如果提供给URL 构造器的urlString 是⽆效的,那么将抛出MalformedURLException。

如果发⽣了别的错误,例如当从连接上获取InputStream 时,那么将抛出IOException。

6.总结
实际上,URLConnection 使⽤套接字从我们指定的URL 中读取信息(它只是解析成IP 地址),但我们⽆须了解它,我们也不关⼼。

但有很多事;我们马上就去看看。

在继续往前讲之前,让我们回顾⼀下创建和使⽤URLConnection 的步骤:
1.⽤您想连接的资源的有效URL String 实例化⼀个URL(如有问题则抛出
MalformedURLException)。

2.打开该URL 上的⼀个连接。

3.把该连接的InputStream 包装进BufferedReader 以使您能够读取⾏。

4.⽤BufferedReader 读⽂档。

5.关闭BufferedReader。

您可在URLClient 的代码清单找到URLClient 的完整代码清单。

四.⼀个简单的⽰例
1.背景
我们将在本部分讨论的⽰例将阐明在Java 代码中如何使⽤Socket 和ServerSocket。

客户机⽤Socket 连接到服务器。

服务器⽤ServerSocket 在端⼝3000 侦听。

客户机请求服务器C: 驱动器上的⽂件内容。

为清楚起见,我们把⽰例分解成客户机端和服务器端。

最后我们将把它们组合起来以使您能看到整体模样。

我们在使⽤JDK 1.2 的IBM VisualAge for Java 3.5 上开发这些代码。

要⾃⼰创建这个⽰例,您应有完好的JDK 1.1.7 或更⾼版本。

客户机和服务器将只在⼀台机器上运⾏,所以您不必担⼼是否有⼀个可⽤的⽹络。

2.创建RemoteFileClient类
这⾥是RemoteFileClient 类的结构:
import java.io.*;
import .*;
public class RemoteFileClient {
protected String hostIp;
protected int hostPort;
protected BufferedReader socketReader;
protected PrintWriter socketWriter;
public RemoteFileClient(String aHostIp, int aHostPort) {
hostIp = aHostIp;
hostPort = aHostPort;
}
public static void main(String[] args) {
}
public void setUpConnection() {
}
public String getFile(String fileNameToGet) {
}
public void tearDownConnection() {
}
}
⾸先我们导⼊ 和java.io。

包为您提供您需要的套接字⼯具。

java.io 包为您提供对流进⾏读写的⼯具,这是您与TCP 套接字通信的唯⼀途径。

我们给我们的类实例变量以⽀持对套接字流的读写和存储我们将连接到的远程主机的详细信息。

我们类的构造器有两个参数:远程主机的IP 地址和端⼝号各⼀个,⽽且构造器将它们赋给实例变量。

我们的类有⼀个main() ⽅法和三个其它⽅法。

稍后我们将探究这些⽅法的细节。

现在您只需知道setUpConnection() 将连接到远程服务器,getFile() 将向远程服务器请求fileNameToGet 的内容以及
3.实现main()
这⾥我们实现main()⽅法,它将创建RemoteFileClient并⽤它来获取远程⽂件的内容,然后打印结果:
public static void main(String[] args) {
RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1", 3000);
remoteFileClient.setUpConnection();
String fileContents =
remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");
remoteFileClient.tearDownConnection();
System.out.println(fileContents);
}
main() ⽅法⽤主机的IP 地址和端⼝号实例化⼀个新RemoteFileClient(客户机)。

然后,我们告诉客户机建⽴⼀个到主机的连接(稍后有更详细的讨论)。

接着,我们告诉客户机获取主机上⼀个指定⽂件的内容。

最后,我们告诉客户机断开它到主机的连接。

我们把⽂件内容打印到控制台,只是为了证明⼀切都是按计划进⾏的。

4.建⽴连接
这⾥我们实现setUpConnection() ⽅法,它将创建我们的Socket 并让我们访问该套接字的流:
public void setUpConnection() {
try {
Socket client = new Socket(hostIp, hostPort);
socketReader = new BufferedReader(
new InputStreamReader(client.getInputStream()));
socketWriter = new PrintWriter(client.getOutputStream());
} catch (UnknownHostException e) {
System.out.println("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);
} catch (IOException e) {
System.out.println("Error setting up socket connection: " + e);
}
}
setUpConnection() ⽅法⽤主机的IP 地址和端⼝号创建⼀个Socket:
Socket client = new Socket(hostIp, hostPort);
我们把Socket 的InputStream 包装进BufferedReader 以使我们能够读取流的⾏。

然后,我们把Socket 的OutputStream 包装进PrintWriter 以使我们能够发送⽂件请求到服务器:
socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
请记住我们的客户机和服务器只是来回传送字节。

客户机和服务器都必须知道另⼀⽅即将发送的是什么以使它们能够作出适当的响应。

在这个案例中,服务器知道我们将发送⼀条有效的⽂件路径。

当您实例化⼀个Socket 时,将抛出UnknownHostException。

这⾥我们不特别处理它,但我们打印⼀些信息到控制台以告诉我们发⽣了什么错误。

同样地,当我们试图获取Socket 的InputStream 或OutputStream 时,如果抛出了⼀个⼀般IOException,我们也打印⼀些信息到控制台。

这是本教程的⼀般做法。

在产品代码中,我们应该做得更完善些。

5.与主机交谈
这⾥我们实现getFile()⽅法,它将告诉服务器我们想要什么⽂件并在服务器传回其内容时接收该内容。

public String getFile(String fileNameToGet) {
StringBuffer fileLines = new StringBuffer();
try {
socketWriter.println(fileNameToGet);
socketWriter.flush();
String line = null;
while ((line = socketReader.readLine()) != null)
fileLines.append(line + "\n");
} catch (IOException e) {
System.out.println("Error reading from file: " + fileNameToGet);
}
return fileLines.toString();
}
对getFile() ⽅法的调⽤要求⼀个有效的⽂件路径String。

它⾸先创建名为fileLines 的StringBuffer,fileLines ⽤于存储我们读⾃服务器上的⽂件的每⼀⾏。

StringBuffer fileLines = new StringBuffer();
在try{}catch{}块中,我们⽤PrintWriter把请求发送到主机,PrintWriter是我们在创建连接期间建⽴的。

socketWriter.println(fileNameToGet);
socketWriter.flush();
请注意这⾥我们是flush() 该PrintWriter,⽽不是关闭它。

这迫使数据被发送到服务器⽽不关闭Socket。

⼀旦我们已经写到Socket,我们就希望有⼀些响应。

我们不得不在Socket的InputStream上等待它,我们通过在while循环中调⽤BufferedReader上的readLine()来达到这个⽬的。

我们把每⼀个返回⾏附加到fileLines StringBuffer(带有⼀个换⾏符以保护⾏):
String line = null;
while ((line = socketReader.readLine()) != null)
fileLines.append(line + "\n");
6.断开连接
这⾥我们实现tearDownConnection()⽅法,它将在我们使⽤完毕连接后负责“清除”:
public void tearDownConnection() {
try {
socketReader.close();
} catch (IOException e) {
System.out.println("Error tearing down socket connection: " + e);
}
}
tearDownConnection() ⽅法只是分别关闭我们在Socket 的InputStream 和OutputStream 上创建的BufferedReader 和
PrintWriter。

这样做会关闭我们从Socket 获取的底层流,所以我们必须捕捉可能的IOException。

7.总结⼀下客户机
我们的类研究完了。

在我们继续往前讨论服务器端的情况之前,让我们回顾⼀下创建和使⽤Socket 的步骤:
1.⽤您想连接的机器的IP 地址和端⼝实例化Socket(如有问题则抛出Exception)。

2.获取Socket 上的流以进⾏读写。

3.把流包装进BufferedReader/PrintWriter 的实例,如果这样做能使事情更简单的话。

4.对Socket 进⾏读写。

5.关闭打开的流。

您可在RemoteFileClient 的代码清单找到RemoteFileClient 的代码清单。

8.创建RemoteFileServer类
这⾥是RemoteFileServer类的结构:
import java.io.*;
import .*;
public class RemoteFileServer {
protected int listenPort = 3000;
public static void main(String[] args) {
}
public void acceptConnections() {
}
public void handleConnection(Socket incomingConnection) {
}
}
跟客户机中⼀样,我们⾸先导⼊ 的java.io。

接着,我们给我们的类⼀个实例变量以保存端⼝,我们从该端⼝侦听进⼊的连接。

缺省情况下,端⼝是3000。

我们的类有⼀个main() ⽅法和两个其它⽅法。

稍后我们将探究这些⽅法的细节。

现在您只需知道acceptConnections() 将允许客户机连接到服务器以及handleConnection() 与客户机Socket 交互以将您所请求的⽂件的内容发送到客户机。

9.实现mian()
这⾥我们实现main()⽅法,它将创建RemoteFileServer并告诉它接受连接:
public static void main(String[] args) {
server.acceptConnections();
}
服务器端的main() ⽅法甚⾄⽐客户机端的更简单。

我们实例化⼀个新RemoteFileServer,它将在缺省侦听端⼝上侦听进⼊的连接请求。

然后我们调⽤acceptConnections() 来告诉该server 进⾏侦听。

10.接受连接
这⾥我们实现acceptConnections()⽅法,它将创建⼀个ServerSocket并等待连接请求:
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
acceptConnections() ⽤欲侦听的端⼝号来创建ServerSocket。

然后我们通过调⽤该ServerSocket 的accept() 来告诉它开始侦听。

accept() ⽅法将造成阻塞直到来了⼀个连接请求。

此时,accept() 返回⼀个新的Socket,这个Socket 绑定到服务器上⼀个随机指定的端⼝,返回的Socket 被传递给handleConnection()。

请注意我们在⼀个⽆限循环中处理对连接的接受。

这⾥不⽀持任何关机。

⽆论何时如果您创建了⼀个⽆法绑定到指定端⼝(可能是因为别的什么控制了该端⼝)的ServerSocket,Java 代码都将抛出⼀个错误。

所以这⾥我们必须捕捉可能的BindException。

就跟在客户机端上时⼀样,我们必须捕捉IOException,当我们试图在ServerSocket上接受连接时,它就会被抛出。

请注意,您可以通过⽤毫秒数调⽤setSoTimeout()来为accept()调⽤设置超时,以避免实际长时间的等待。

调⽤setSoTimeout()将使accept()经过指定占⽤时间后抛出IOException。

11.处理连接
这⾥我们实现handleConnection()⽅法,它将⽤连接的流来接收输⼊和写输出:
public void handleConnection(Socket incomingConnection) {
try {
OutputStream outputToSocket = incomingConnection.getOutputStream();
InputStream inputFromSocket = incomingConnection.getInputStream();
BufferedReader streamReader =
new BufferedReader(new InputStreamReader(inputFromSocket));
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
PrintWriter streamWriter =
new PrintWriter(incomingConnection.getOutputStream());
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}
fileReader.close();
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}
跟在客户机中⼀样,我们⽤getOutputStream()和getInputStream()来获取与我们刚创建的Socket相关联的流。

跟在客户机端⼀样,我们把InputStream包装进BufferedReader,把OutputStream包装进PrintWriter。

在服务器端上,我们需要添加⼀些代码,⽤来读取⽬标⽂件和把内容逐⾏发送到客户机。

这⾥是重要的代码:
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
BufferedReader bufferedFileReader = new BufferedReader(fileReader);
String line = null;
while ((line = bufferedFileReader.readLine()) != null) {
streamWriter.println(line);
}
这些代码值得详细解释。

让我们⼀点⼀点来看:
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
⾸先,我们使⽤Socket的InputStream的BufferedReader。

我们应该获取⼀条有效的⽂件路径,所以我们⽤该路径名构造⼀个新File。

我们创建⼀个新FileReader来处理读⽂件的操作。

BufferedReader bufferedFileReader = new BufferedReader(fileReader);
这⾥我们把FileReader 包装进BufferedReader 以使我们能够逐⾏地读该⽂件。

接着,我们调⽤BufferedReader 的readLine()。

这个调⽤将造成阻塞直到有字节到来。

我们获取⼀些字节之后就把它们放到本地的line 变量中,然后再写出到客户机上。

完成读写操作之后,我们就关闭打开的流。

请注意我们在完成从Socket 的读操作之后关闭streamWriter 和streamReader。

您或许会问我们为什么不在读取⽂件名之后⽴刻关闭streamReader。

原因是当您这样做时,您的客户机将不会获取任何数据。

如果您在关闭streamWriter 之前关闭streamReader,则您可以往Socket 写任何东西,但却没有任何数据能通过通道(通道被关闭了)。

12.总结⼀下服务器
在我们接着讨论另⼀个更实际的⽰例之前,让我们回顾⼀下创建和使⽤ServerSocket 的步骤:
1.⽤⼀个您想让它侦听传⼊客户机连接的端⼝来实例化⼀个ServerSocket(如有问题则抛
出Exception)。

2.调⽤ServerSocket 的accept() 以在等待连接期间造成阻塞。

3.获取位于该底层Socket 的流以进⾏读写操作。

4.按使事情简单化的原则包装流。

5.对Socket 进⾏读写。

您可在RemoteFileServer 的代码清单找到RemoteFileServer 的完整的代码清单。

五.⼀个多线程的⽰例
1.介绍
前⾯的⽰例教给您基础知识,但并不能令您更深⼊。

如果您到此就停⽌了,那么您⼀次只能处理⼀台客户机。

原因是handleConnection() 是⼀个阻塞⽅法。

只有当它完成了对当前连接的处理时,服务器才能接受另⼀个客户机。

在多数时候,您将需要(也有必要)⼀个多线程服务器。

要开始同时处理多台客户机,并不需要对RemoteFileServer 作太多改变。

事实上,要是我们前⾯讨论过待发(backlog),那
我们就只需改变⼀个⽅法,虽然我们将需要创建⼀些新东西来处理进⼊的连接。

这⾥我们还将向您展⽰ServerSocket 如何处理众多等待(备份)使⽤服务器的客户机。

本⽰例对线程的低效使⽤,所以请耐⼼点。

2.接受(太多)连接
这⾥我们实现改动过的acceptConnections()⽅法,它将创建⼀个能够处理待发请求的ServerSocket,并告诉ServerSocket接受连接:
public void acceptConnections() {
try {
ServerSocket server = new ServerSocket(listenPort, 5);
Socket incomingConnection = null;
while (true) {
incomingConnection = server.accept();
handleConnection(incomingConnection);
}
} catch (BindException e) {
System.out.println("Unable to bind to port " + listenPort);
} catch (IOException e) {
System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);
}
}
新的server 仍然需要acceptConnections(),所以这些代码实际上是⼀样的。

突出显⽰的⾏表⽰⼀个重⼤的不同。

对这个多线程版,我们现在可以指定客户机请求的最⼤数⽬,这些请求都能在实例化ServerSocket 期间处于待发状态。

如果我们没有指定客户机请求的最⼤数⽬,则我们假设使⽤缺省值50。

这⾥是它的⼯作机制。

假设我们指定待发数(backlog 值)是5 并且有五台客户机请求连接到我们的服务器。

我们的服务器将着⼿处理第⼀个连接,但处理该连接需要很长时间。

由于我们的待发值是5,所以我们⼀次可以放五个请求到队列中。

我们正在处理⼀个,所以这意味着还有其它五个正在等待。

等待的和正在处理的⼀共有六个。

当我们的服务器仍忙于接受⼀号连接(记住队列中还有2—6 号)时,如果有第七个客户机提出连接申请,那么,该第七个客户机将遭到拒绝。

我们将在带有连接池服务器⽰例中说明如何限定能同时连接的客户机数⽬。

3.处理连接:第⼀部分
这⾥我们将讨论handleConnection()⽅法的结构,这个⽅法⽣成⼀个新的Thread来处理每个连接。

我们将分两部分讨论这个问题。

这⼀屏我们将着重该⽅法本⾝,然后在下⼀屏研究该⽅法所使⽤的
public void handleConnection(Socket connectionToHandle) {
new Thread(new ConnectionHandler(connectionToHandle)).start();
}
我们对RemoteFileServer 所做的⼤改动就体现在这个⽅法上。

我们仍然在服务器接受⼀个连接之后调⽤handleConnection(),但现在我们把该Socket 传递给ConnectionHandler 的⼀个实例,它是Runnable 的。

我们⽤ConnectionHandler 创建⼀个新Thread 并启动它。

ConnectionHandler 的run() ⽅法包含Socket 读/写和读File 的代码,这些代码原来在RemoteFileServer 的handleConnection() 中。

4.处理连接:第⼆部分
这⾥是ConnectionHandler类的结构:
import java.io.*;
import .*;
public class ConnectionHandler implements Runnable{
Socket socketToHandle;
public ConnectionHandler(Socket aSocketToHandle) {
socketToHandle = aSocketToHandle;
}
public void run() {
}
}
这个助⼿类相当简单。

跟我们到⽬前为⽌的其它类⼀样,我们导⼊ 和java.io。

该类只有⼀个实例变量socketToHandle,它保存由该实例处理的Socket。

类的构造器⽤⼀个Socket 实例作参数并将它赋给socketToHandle。

请注意该类实现了Runnable 接⼝。

实现这个接⼝的类都必须实现run() ⽅法,我们的类就是这样做的。

稍后我们将探究run()的细节。

现在只需知道它将实际处理连接,所⽤的代码跟我们先前在RemoteFileServer 类中看到的是⼀样的。

5.实现run()
这⾥我们实现run() ⽅法,它将攫取我们的连接的流,⽤它来读写该连接,并在任务完成之后关闭它:
public void run() {
try {
PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());
BufferedReader streamReader =
new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));
String fileToRead = streamReader.readLine();
BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));
String line = null;
while ((line = fileReader.readLine()) != null)
streamWriter.println(line);
streamWriter.close();
streamReader.close();
} catch (Exception e) {
System.out.println("Error handling a client: " + e);
}
}
ConnectionHandler 的run() ⽅法所做的事情就是RemoteFileServer 上的handleConnection() 所做的事情。

⾸先,我们把InputStream 和OutputStream 分别包装(⽤Socket 的getOutputStream() 和getInputStream())进BufferedReader 和PrintWriter。

然后我们⽤这些代码逐⾏地读⽬标⽂件:
FileReader fileReader = new FileReader(new File(streamReader.readLine()));
BufferedReader bufferedFileReader = new BufferedReader(fileReader);。

相关文档
最新文档