`
javathinker
  • 浏览: 235674 次
  • 来自: ...
文章分类
社区版块
存档分类
最新评论

Java 套接字(Socket)详解

阅读更多

套接字(socket)为两台计算机之间的通信提供了一种机制,在JamesGosling注意到Java语言之前,套接字就早已赫赫有名。该语言只是让您不必了解底层操作系统的细节就能有效地使用套接字。

1 客户机/服务器模型

在饭店里,菜单上各种具有异国情调的食品映入你的眼帘,于是你要了一份pizza。几分钟后,你用力咀嚼浇着融化的乳酪和其他你喜欢的配料的热pizza。你不知道,也不想知道:侍者从那里弄来了pizza,在制作过程中加进了什么,以及配料是如何获得的。

上例中包含的实体有:美味的pizza、接受你定餐的侍者、制作pizza的厨房,当然还有你。你是定pizza的顾客或客户。制作pizza的过程对于你而言是被封装的。你的请求在厨房中被处理,pizza制作完成后,由侍者端给你。

你所看到的就是一个客户机/服务器模型。客户机向服务器发送一个请求或命令。服务器处理客户机的请求。客户机和服务器之间的通讯是客户机/服务器模型中的一个重要组成部分,通常通过网络进行。

客户机/服务器模型是一个应用程序开发框架,该框架是为了将数据的表示与其内部的处理和存储分离开来而设计的。客户机请求服务,服务器为这些请求服务。请求通过网络从客户机传递到服务器。服务器所进行的处理对客户机而言是隐藏的。一个服务器可以为多台客户机服务。

多台客户机访问服务器

服务器和客户机不一定是硬件组件。它们可以是工作啊同一机器或不同机器上的程序。、

考虑一个航空定票系统中的数据输入程序:数据----乘客名、航班号、飞行日期、目的地等可以被输入到前端----客户机的应用程序中。一旦数据输入之后,客户机将数据发送到后端----服务器端。服务器处理数据并在数据库中保存数据。客户机/服务器模型的重要性在于所有的数据都存放在同一地点。客户机从不同的地方访问同一数据源,服务器对所有的输入数据应用同样的检验规则。

万维网为‘为什么要将数据的表示与其存储、处理分离开来’提供了一个很好的例子。在Web上,你无需控制最终用户用来访问你数据的平台和软件。你可以考虑编写出适用与每一种潜在的目标平台的应用程序。

‘客户机/服务器应用程序的服务器部分’管理通过多个客户机访问服务器的、多个用户共享的资源。表明‘客户机/服务器程序的服务器部分’强大功能的最好例子应该是Web服务器,它通过Internet将HTML页传递给不同的Web用户。

Java编程语言中最基本的特点是在Java中创建的程序的代码的可移植性。因为具有其他语言所不具备的代码可移植性,Java允许用户只要编写一次应用程序,就可以在任何客户机系统上发布它,并可以让客户机系统解释该程序。这意味着:你只要写一次代码,就能使其在任何平台上运行。

2 协议

当你同朋友交谈时,你们遵循一些暗含的规则(或协议)。例如:你们俩不能同时开始说话,或连续不间断地说话。如果你们这样作的话,谁也不能理解对方所说的东西。当你说话时,你的朋友倾听,反之亦然。你们以双方都能理解的语言和速度进行对话。

当计算机之间进行通讯的时候,也需要遵循一定的规则。数据以包的形式从一台机器发送到另一台。这些规则管理数据打包、数据传输速度和重新数据将其恢复成原始形式。这些规则被称为网络协议。网络协议是通过网络进行通讯的系统所遵循的一系列规则和惯例。连网软件通常实现有高低层次之分的多层协议。网络协议的例子有:TCP/IP、UDP、AppleTalk和NetBEUI。

Java提供了一个丰富的、支持网络的类库,这些类使得应用程序能方便地访问网络资源。Java提供了两种通讯工具。它们是:使用用户报文协议(UDP)的报文和使用传输控制协议/因特网协议(TCP/IP)的Sockets(套接字)。

数据报包是一个字节数组从一个程序(发送程序)传送到另一个(接受程序)。由于数据报遵守UDP,不保证发出的数据包必须到达目的地。数据报并不是可信赖的。因此,仅当传送少量数据时才使用,而且发送者和接受者之间的距离间隔不大,假如是网络交通高峰,或接受程序正处理来自其他程序的多个请求,就有机会出现数据报包的丢失。

Sockets套接字用TCP来进行通讯。套接字模型同其他模型相比,优越性在于其不受客户请求来自何处的影响。只要客户机遵循TCP/IP协议,服务器就会对它的请求提供服务。这意味着客户机可以是任何类型的计算机。客户机不再局限为UNIX、Windows、DOS或Macintosh平台,因此,网上所有遵循TCP/IP协议的计算机可以通过套接字互相通讯。

3 Sockets套接字

3.1 Sockets概况

在客户机/服务器应用程序中,服务器提供象处理数据库查询或修改数据库中的数据之类的服务。发生在客户机和服务器之间的通讯必须是可靠的,同时数据在客户机上的次序应该和服务器发送出来的次序相同。

什么是套接字?

既然我们已经知道套接字扮演的角色,那么剩下的问题是:什么是套接字?BruceEckel在他的《Java编程思想》一书中这样描述套接字:套接字是一种软件抽象,用于表达两台机器之间的连接“终端”。对于一个给定的连接,每台机器上都有一个套接字,您也可以想象它们之间有一条虚拟的“电缆”,“电缆”的每一端都插入到套接字中。当然,机器之间的物理硬件和电缆连接都是完全未知的。抽象的全部目的是使我们无须知道不必知道的细节。

简言之,一台机器上的套接字与另一台机器上的套接字交谈就创建一条通信通道。程序员可以用该通道来在两台机器之间发送数据。当您发送数据时,TCP/IP协议栈的每一层都会添加适当的报头信息来包装数据。这些报头帮助协议栈把您的数据送到目的地。好消息是Java语言通过"流"为您的代码提供数据,从而隐藏了所有这些细节,这也是为什么它们有时候被叫做流套接字(streamingsocket)的原因。

把套接字想成两端电话上的听筒,我和您通过专用通道在我们的电话听筒上讲话和聆听。直到我们决定挂断电话,对话才会结束(除非我们在使用蜂窝电话)。而且我们各自的电话线路都占线,直到我们挂断电话。

如果想在没有更高级机制如ORB(以及CORBA、RMI、IIOP等等)开销的情况下进行两台计算机之间的通信,那么套接字就适合您。套接字的低级细节相当棘手。幸运的是,Java平台给了您一些虽然简单但却强大的更高级抽象,使您可以容易地创建和使用套接字。

传输控制协议(TCP)提供了一条可靠的、点对点的通讯通道,客户机/服务器应用程序可以用该通道互相通讯。要通过TCP进行通讯,客户机和服务器程序建立连接并绑定套接字。套接字用于处理通过网络连接的应用程序之间的通讯。客户机和服务器之间更深入的通讯通过套接字完成。

Java被设计成一种连网语言。它通过将连接功能封装到套接字类里而使得网络编程更加容易。套接字类即Socket类(它创建一个客户套接字)和ServerSocket类(它创建一个服务器套接字)。套接字类大致介绍如下:

lSocket是基类,它支持TCP协议。TCP是一个可靠的流网络连接协议。Socket类提供了流输入/输出的方法,使得从套接字中读出数据和往套接字中写数据都很容易。该类对于编写因特网上的通讯程序而言是必不可少的。

lServerSocket是一个因特网服务程序用来监听客户请求的类。ServerSocket实际上并不执行服务;而是创建了一个Socket对象来代表客户机。通讯由创建的对象来完成。

3.2 IP地址和端口

因特网服务器可以被认为是一组套接字类,它们提供了一般称为服务的附加功能。服务的例子有:电子邮件、远程登录的Telnet、和通过网络传输文件的文件传输协议(FTP)。每种服务都与一个端口相联系。端口是一个数值地址,通过它来处理服务请求(就象请求Web页一样)。

TCP协议需要两个数据项:IP地址和端口号。因此,当键入http://www.jinnuo.com时,你是如何进入金诺的主页呢?

因特网协议(IP)提供每一项网络设备。这些设备都带有一个称为IP地址的逻辑地址。由因特网协议提供的IP地址具有特定的形式。每个IP地址都是32位的数值,表示4个范围在0到255之间的8位数值金诺已经注册了它的名字,分配给http://www.jinnuo.com的IP地址为192.168.0.110。

注意:域名服务或DNS服务是将http://www.jinnuo.com翻译成192.168.0.110的服务。这使你可以键入http://www.jinnuo.com而不必记住IP地址。想象一下,怎么可能记住所有需要访问的站点的IP地址!有趣的是一个网络名可以映射到许多IP地址。对于经常访问的站点可能需要这一功能,因为这些站点容纳大量的信息,并需要多个IP地址来提供业务服务。例如:192.168.0.110的实际的内部名称为http://www.jinnuo.com。DNS可以将分配给jinnuoLtd.的一系列IP地址翻译成http://www.jinnuo.com。

如果没有指明端口号,则使用服务文件中服务器的端口。每种协议有一个缺省的端口号,在端口号未指明时使用该缺省端口号。

端口号应用

21FTP.传输文件

23Telnet.提供远程登录

25SMTP.传递邮件信息

67BOOTP.在启动时提供配置情况

80HTTP.传输Web页

109POP.使用户能访问远程系统中的邮箱

让我们再来看一下URL:http://www.jinnuo.com

URL的第一部分(http)意味着你正在使用超文本传输协议(HTTP),该协议处理Web文档。如果没有指明文件,大多数的Web服务器会取一个叫index.html文件。因此,IP地址和端口既可以通过明确指出URL各部分来决定,也可以由缺省值决定。

4 创建Socket客户

我们将在本部分讨论的示例将阐明在Java代码中如何使用Socket和ServerSocket。客户机用Socket连接到服务器。服务器用ServerSocket在端口1001侦听。客户机请求服务器C:驱动器上的文件内容。

创建RemoteFileClient类
import java.io.*;
import java.net.*;
public class RemoteFileClient{

protected BufferedReader socketReader;
protected PrintWriter socketWriter;
protected String hostIp;
protected int hostPort;
//构造方法

public RemoteFileClient(String hostIp,inthost Port){

this.hostIp=hostIp;

this.hostPort=hostPort;

}
//向服务器请求文件的内容
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(IOExceptione){

System.out.println("Errorreadingfromfile:"+fileNameToGet);

}

return fileLines.toString();

}//连接到远程服务器
public void setUpConnection(){

try{

Socketclient=new Socket(hostIp,hostPort);
socketReader=new BufferedReder(new InputStreamReader (

client.getInputStream()));
socketWriter=new PrintWriter(client.getOutputStream());

}catch(UnknownHostExceptione){

System.out.println(

"Error1settingupsocketconnection:unknownhostat"+hostIp+

":"+hostPort);

}catch(IOExceptione){

System.out.println(

"Error2settingupsocketconnection:"+e);

}

}
//断开远程服务器
public void tearDownConnection(){

try{

socketWriter.close();
socketReader.close();

}catch(IOExceptione){

System.out.println(

"Errortearingdownsocketconnection:"+e);

}

}

public static void main(Stringargs[]){

RemoteFileClient remoteFileClient=

new RemoteFileClient("127.0.0.1",1001);
remoteFileClient.setUpConnection();
String BufferfileContents=new StringBuffer();
fileContents.append(remoteFileClient.getFile

("RemoteFileServer.java"));
//remoteFileClient.tearDownConnection();
System.out.println(fileContents);

}

}

首先我们导入java.net和java.io。java.net包为您提供您需要的套接字工具。java.io包为您提供对流进行读写的工具,这是您与TCP套接字通信的唯一途径。

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

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

我们的类有一个main()方法和三个其它方法。稍后我们将探究这些方法的细节。现在您只需知道setUpConnection()将连接到远程服务器,getFile()将向远程服务器请求fileNameToGet的内容以及tearDownConnection()将从远程服务器上断开。

实现main()

这里我们实现main()方法,它将创建RemoteFileClient并用它来获取远程文件的内容,然后打印结果。main()方法用主机的IP地址和端口号实例化一个新RemoteFileClient(客户机)。然后,我们告诉客户机建立一个到主机的连接。接着,我们告诉客户机获取主机上一个指定文件的内容。最后,我们告诉客户机断开它到主机的连接。我们把文件内容打印到控制台,只是为了证明一切都是按计划进行的。

建立连接

这里我们实现setUpConnection()方法,它将创建我们的Socket并让我们访问该套接字的流:
public void setUpConnection(){

try{

Socketclient=new Socket(hostIp,hostPort);
socketReader=new BufferedReader(new InputStreamReader

(client.getInputStream()));
socketWriter=newPrintWriter(client.getOutputStream());

}catch(UnknownHostExceptione){

System.out.println(

"Error1settingupsocketconnection:unknownhostat"

+hostIp+":"+hostPort);

}catch(IOExceptione){

System.out.println(

"Error2settingupsocketconnection:"+e);

}

}

setUpConnection()方法用主机的IP地址和端口号创建一个Socket:

Socketclient=newSocket(hostIp,hostPort);

我们把Socket的InputStream包装进BufferedReader以使我们能够读取流的行。然后,我们把Socket的OutputStream包装进PrintWriter以使我们能够发送文件请求到服务器:
socketReader=new BufferedReader(new InputStreamReader(client.getInputStream()));
socketWriter=new PrintWriter(client.getOutputStream());

请记住我们的客户机和服务器只是来回传送字节。客户机和服务器都必须知道另一方即将发送的是什么以使它们能够作出适当的响应。在这个案例中,服务器知道我们将发送一条有效的文件路径。

当您实例化一个Socket时,将抛出UnknownHostException。这里我们不特别处理它,但我们打印一些信息到控制台以告诉我们发生了什么错误。同样地,当我们试图获取Socket的InputStream或OutputStream时,如果抛出了一个一般IOException,我们也打印一些信息到控制台。

与主机交谈

这里我们实现getFile()方法,它将告诉服务器我们想要什么文件并在服务器传回其内容时接收该内容。
public StringgetFile(StringfileNameToGet){

StringBufferfileLines=newStringBuffer();
try{

socketWriter.println(fileNameToGet);
socketWriter.flush();
Stringline=null;
while((line=socketReader.readLine())!=null)

fileLines.append(line+"\n");

}catch(IOExceptione){

System.out.println(

"Errorreadingfromfile:"+fileNameToGet);

}

return fileLines.toString();

}

对getFile()方法的调用要求一个有效的文件路径String。它首先创建名为fileLines的StringBuffer,fileLines用于存储我们读自服务器上的文件的每一行。

StringBufferfileLines=newStringBuffer();

在try{}catch{}块中,我们用PrintWriter把请求发送到主机,PrintWriter是我们在创建连接期间建立的。

socketWriter.println(fileNameToGet);socketWriter.flush();

请注意这里我们是flush()该PrintWriter,而不是关闭它。这迫使数据被发送到服务器而不关闭Socket。

一旦我们已经写到Socket,我们就希望有一些响应。我们不得不在Socket的InputStream上等待它,我们通过在while循环中调用BufferedReader上的readLine()来达到这个目的。我们把每一个返回行附加到fileLinesStringBuffer(带有一个换行符以保护行):

Stringline=null;

while((line=socketReader.readLine())!=null)

fileLines.append(line+"\n");

断开连接

这里我们实现tearDownConnection()方法,它将在我们使用完毕连接后负责“清除”。tearDownConnection()方法只是分别关闭我们在Socket的InputStream和OutputStream上创建的BufferedReader和PrintWriter。这样做会关闭我们从Socket获取的底层流,所以我们必须捕捉可能的IOException。

总结一下客户机

让我们回顾一下创建和使用Socket的步骤:

1.用您想连接的机器的IP地址和端口实例化Socket(如有问题则抛出Exception)。

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

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

4.对Socket进行读写。

5.关闭打开的流。

5创建服务器Socket

创建RemoteFileServer类
import java.io.*;
import java.net.*;
public class RemoteFileServer{

int listenPort;

public RemoteFileServer(intlistenPort){

this.listenPort=listenPort;

}
//允许客户机连接到服务器,等待客户机请求
public void acceptConnections(){

try{

ServerSocket server=new ServerSocket(listenPort);
SocketincomingConnection=null;
while(true){

incomingConnection=server.accept();
handleConnection(incomingConnection);

}

}catch(BindExceptione){

System.out.println("Unabletobindtoport"+listenPort);

}catch(IOExceptione){

System.out.println(

"UnabletoinstantiateaServerSocketonport:"

+listenPort);}}
//与客户机Socket交互以将客户机所请求的文件的内容发送到客户机
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()));
BufferedReader bufferedFileReader=new

BufferedReader(fileReader);
PrintWriterstream Writer=newPrintWriter(

incomingConnection.getOutputStream());
Stringline=null;

while((line=bufferedFileReader.readLine())!=null){

streamWriter.println(line);}fileReader.close();
streamWriter.close();
streamReader.close();

}catch(Exceptione){

System.out.println("Errorhandlingaclient:"+e);
e.printStackTrace();

}

}
public static void main(Stringargs[]){

RemoteFileServer server=new RemoteFileServer(1001);
server.acceptConnections();

}

}

跟客户机中一样,我们首先导入java.net的java.io。接着,我们给我们的类一个实例变量以保存端口,我们从该端口侦听进入的连接。缺省情况下,端口是1001。

我们的类有一个main()方法和两个其它方法。稍后我们将探究这些方法的细节。现在您只需知道acceptConnections()将允许客户机连接到服务器以及handleConnection()与客户机Socket交互以将您所请求的文件的内容发送到客户机。

实现main()

这里我们实现main()方法,它将创建RemoteFileServer并告诉它接受连接:服务器端的main()方法中,我们实例化一个新RemoteFileServer,它将在侦听端口(1001)上侦听进入的连接请求。然后我们调用acceptConnections()来告诉该server进行侦听。

接受连接

这里我们实现acceptConnections()方法,它将创建一个ServerSocket并等待连接请求:
public void acceptConnections(){

try{

ServerSocketserver=new ServerSocket(listenPort);
SocketincomingConnection=null;
while(true){

incomingConnection=server.accept();
handleConnection(incomingConnection);

}

}catch(BindExceptione){

System.out.println("Unabletobindtoport"+listenPort);

}catch(IOExceptione){

System.out.println(

"UnabletoinstantiateaServerSocketonport:"

+listenPort);

}}

acceptConnections()用欲侦听的端口号来创建ServerSocket。然后我们通过调用该ServerSocket的accept()来告诉它开始侦听。accept()方法将造成阻塞直到来了一个连接请求。此时,accept()返回一个新的Socket,这个Socket绑定到服务器上一个随机指定的端口,返回的Socket被传递给handleConnection()。请注意我们在一个无限循环中处理对连接的接受。这里不支持任何关机。

无论何时如果您创建了一个无法绑定到指定端口(可能是因为别的什么控制了该端口)的ServerSocket,Java代码都将抛出一个错误。所以这里我们必须捕捉可能的BindException。就跟在客户机端上时一样,我们必须捕捉IOException,当我们试图在ServerSocket上接受连接时,它就会被抛出。请注意,您可以通过用毫秒数调用setSoTimeout()来为accept()调用设置超时,以避免实际长时间的等待。调用setSoTimeout()将使accept()经过指定占用时间后抛出IOException。

处理连接

这里我们实现handleConnection()方法,它将用连接的流来接收输入和写输出:
public void handleConnection(SocketincomingConnection){
try{

OutputStreamoutputToSocket=incomingConnection.getOutputStream();
InputStream inputFromSocket=incomingConnection.getInputStream();
BufferedReaderstream Reader=newBufferedReader(

newInputStreamReader(inputFromSocket));
FileReader fileReader=new FileReader(

new File(streamReader.readLine()));
BufferedReader bufferedFileReader=new BufferedReader(

fileReader);
PrintWriterstream Writer=newPrintWriter(

incomingConnection.getOutputStream());
String line=null;
while((line=bufferedFileReader.readLine())!=null){

streamWriter.println(line);

}

fileReader.close();
streamWriter.close();

streamReader.close();

}catch(Exceptione){

System.out.println("Errorhandlingaclient:"+e);
e.printStackTrace();

}

}

跟在客户机中一样,我们用getOutputStream()和getInputStream()来获取与我们刚创建的Socket相关联的流。跟在客户机端一样,我们把InputStream包装进BufferedReader,把OutputStream包装进PrintWriter。在服务器端上,我们需要添加一些代码,用来读取目标文件和把内容逐行发送到客户机。这里是重要的代码:
FileReader fileReader=new FileReader(new File(streamReader.readLine()));
BufferedReader bufferedFileReader=new BufferedReader(fileReader);
Stringline=null;
while((line=bufferedFileReader.readLine())!=null){

streamWriter.println(line);

}

这些代码值得详细解释。让我们一点一点来看:

FileReaderfileReader=newFileReader(newFile(streamReader.readLine()));

首先,我们使用Socket的InputStream的BufferedReader。我们应该获取一条有效的文件路径,所以我们用该路径名构造一个新File。我们创建一个新FileReader来处理读文件的操作。

BufferedReaderbufferedFileReader=newBufferedReader(fileReader);

这里我们把FileReader包装进BufferedReader以使我们能够逐行地读该文件。

接着,我们调用BufferedReader的readLine()。这个调用将造成阻塞直到有字节到来。我们获取一些字节之后就把它们放到本地的line变量中,然后再写出到客户机上。完成读写操作之后,我们就关闭打开的流。

请注意我们在完成从Socket的读操作之后关闭streamWriter和streamReader。您或许会问我们为什么不在读取文件名之后立刻关闭streamReader。原因是当您这样做时,您的客户机将不会获取任何数据。如果您在关闭streamWriter之前关闭streamReader,则您可以往Socket写任何东西,但却没有任何数据能通过通道(通道被关闭了)。

总结一下服务器

在我们接着讨论另一个更实际的示例之前,让我们回顾一下创建和使用ServerSocket的步骤:

1.用一个您想让它侦听传入客户机连接的端口来实例化一个ServerSocket(如有问题则抛出Exception)。

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

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

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

5.对Socket进行读写。

6.关闭打开的流(并请记住,永远不要在关闭Writer之前关闭Reader)。

6创建多线程Socket服务器

前面的示例教给您基础知识,但并不能令您更深入。如果您到此就停止了,那么您一次只能处理一台客户机。原因是handleConnection()是一个阻塞方法。只有当它完成了对当前连接的处理时,服务器才能接受另一个客户机。在多数时候,您将需要(也有必要)一个多线程服务器。

创建MultithreadedRemoteFileServer类
import java.io.*;
import java.net.*;
public class MultithreadedRemoteFileServer{

intlistenPort;
public MultithreadedRemoteFileServer(intlistenPort){

this.listenPort=listenPort;

}
//允许客户机连接到服务器,等待客户机请求
public void acceptConnections(){

try{

ServerSocketserver=new ServerSocket(listenPort,5);
SocketincomingConnection=null;
while(true){

incomingConnection=server.accept();
handleConnection(incomingConnection);

}

}catch(BindExceptione){

System.out.println("Unabletobindtoport"+listenPort);

}catch(IOExceptione){

System.out.println(

"UnabletoinstantiateaServerSocketonport:"

+listenPort);

}}
//与客户机Socket交互以将客户机所请求的文件的内容发送到客户机
public void handleConnection(Socket connectionToHandle){

new Thread(new ConnectionHandler(connectionToHandle)).start();

}
public staticvoidmain(Stringargs[]){

MultithreadedRemoteFileServer server=new

MultithreadedRemoteFileServer(1001);
server.acceptConnections();

}}

这里我们实现改动过acceptConnections()方法,它将创建一个能够处理待发请求的ServerSocket,并告诉ServerSocket接受连接。

新的server仍然需要acceptConnections(),所以这些代码实际上是一样的。突出显示的行表示一个重大的不同。对这个多线程版,我们现在可以指定客户机请求的最大数目,这些请求都能在实例化ServerSocket期间处于待发状态。如果我们没有指定客户机请求的最大数目,则我们假设使用缺省值50。

这里是它的工作机制。假设我们指定待发数(backlog值)是5并且有五台客户机请求连接到我们的服务器。我们的服务器将着手处理第一个连接,但处理该连接需要很长时间。由于我们的待发值是5,所以我们一次可以放五个请求到队列中。我们正在处理一个,所以这意味着还有其它五个正在等待。等待的和正在处理的一共有六个。当我们的服务器仍忙于接受一号连接(记住队列中还有2?6号)时,如果有第七个客户机提出连接申请,那么,该第七个客户机将遭到拒绝。我们将在带有连接池服务器示例中说明如何限定能同时连接的客户机数目。

处理连接:

publicvoidhandleConnection(SocketconnectionToHandle){newThread(newConnectionHandler(connectionToHandle)).start();}

我们对RemoteFileServer所做的大改动就体现在这个方法上。我们仍然在服务器接受一个连接之后调用handleConnection(),但现在我们把该Socket传递给ConnectionHandler的一个实例,它是Runnable的。我们用ConnectionHandler创建一个新Thread并启动它。ConnectionHandler的run()方法包Socket读/写和读File的代码,这些代码原来在RemoteFileServer的handleConnection()中。

创建ConnectionHandler类
import java.io.*;
import java.net.*;
public class ConnectionHandler implements Runnable{

protected Socket socketToHandle;
public ConnectionHandler(SocketsocketToHandle){

this.socketToHandle=socketToHandle;

}
public void run(){

try{

PrintWriterstreamWriter=newPrintWriter(

socketToHandle.getOutputStream());
BufferedReaderstream Reader=newBufferedReader(

new InputStreamReader(socketToHandle.getInputStream()));
String fileToRead=streamReader.readLine();
BufferedReader fileReader=newBufferedReader(

new FileReader(fileToRead));
Stringline=null;
while((line=fileReader.readLine())!=null){

streamWriter.println(line);

}

fileReader.close();
streamWriter.close();

streamReader.close();

}catch(Exceptione){

System.out.println("Errorhandlingaclient:"+e);
e.printStackTrace();

}}}

这个助手类相当简单。跟我们到目前为止的其它类一样,我们导入java.net和java.io。该类只有一个实例变量socketToHandle,它保存由该实例处理的Socket。

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

请注意该类实现了Runnable接口。实现这个接口的类都必须实现run()方法。这里我们实现run()方法,它将攫取我们的连接的流,用它来读写该连接,并在任务完成之后关闭它。ConnectionHandler的run()方法所做的事情就是RemoteFileServer上的handleConnection()所做的事情。首先,我们把InputStream和OutputStream分别包装(用Socket的getOutputStream()和getInputStream())进BufferedReader和PrintWriter。然后我们用这些代码逐行地读目标文件:
PrintWriterstream Writer=new PrintWriter(

socketToHandle.getOutputStream());
BufferedReaderstream Reader=new BufferedReader(

new InputStreamReader(socketToHandle.getInputStream()));
String fileToRead=streamReader.readLine();

BufferedReaderfile Reader=new BufferedReader(

new FileReader(fileToRead));
String line=null;
while((line=fileReader.readLine())!=null){

streamWriter.println(line);

}

请记住我们应该从客户机获取一条有效的文件路径,这样用该路径名构造一个新File,把它包装进FileReader以处理读文件的操作,然后把它包装进BufferedReader以让我们逐行地读该文件。我们while循环中调用BufferedReader上的readLine()直到不再有要读的行。请记注,对readLine()的调用将造成阻塞,直到有字节来到为止。我们获取一些字节之后就把它们放到本地的line变量中,然后写出到客户机上。完成读写操作之后,我们关闭打开的流。

总结一下多线程服务器

让我们回顾一下创建和使用“多线程版”的服务器的步骤:

1.修改acceptConnections()以用缺省为50(或任何您想要的大于1的指定数字)实例化ServerSocket。

2.修改ServerSocket的handleConnection()以用ConnectionHandler的一个实例生成一个新的Thread。

3.借用RemoteFileServer的handleConnection()方法的代码实现ConnectionHandler类。

7创建带有连接池的Socket服务器

我们现在已经拥有的MultithreadedServer每当有客户机申请一个连接时都在一个新Thread中创建一个新ConnectionHandler。这意味着可能有一捆Thread“躺”在我们周围。而且创建Thread的系统开销并不是微不足道的。如果性能成为了问题(也请不要事到临头才意识到它),更高效地处理我们的服务器是件好事。那么,我们如何更高效地管理服务器端呢?我们可以维护一个进入的连接池,一定数量的ConnectionHandler将为它提供服务。这种设计能带来以下好处:

"它限定了允许同时连接的数目。

"我们只需启动ConnectionHandlerThread一次。

幸运的是,跟在我们的多线程示例中一样,往代码中添加“池”不需要来一个大改动。事实上,应用程序的客户机端根本就不受影响。在服务器端,我们在服务器启动时创建一定数量的ConnectionHandler,我们把进入的连接放入“池”中并让ConnectionHandler打理剩下的事情。这种设计中有很多我们不打算讨论的可能存在的技巧。例如,我们可以通过限定允许在“池”中建立的连接的数目来拒绝客户机。

请注意:我们将不会再次讨论acceptConnections()。这个方法跟前面示例中的完全一样。它无限循环地调用ServerSocket上的accept()并把连接传递到handleConnection()。

创建PooledRemoteFileServer类
importjava.io.*;
importjava.net.*;
importjava.util.*;
publicclassPooledRemoteFileServer{protectedintmaxConnections;
protectedintlistenPort;protectedServerSocketserverSocket;
publicPooledRemoteFileServer(intaListenPort,intmaxConnections){listenPort=aListenPort;
this.maxConnections=maxConnections;}
publicvoidacceptConnections(){try{ServerSocketserver=newServerSocket(listenPort,5);
SocketincomingConnection=null;
while(true){incomingConnection=server.accept();
handleConnection(incomingConnection);}}catch(BindExceptione){System.out.println("");}
catch(IOExceptione){System.out.println(""+listenPort);}}protectedvoidhandleConnection(SocketconnectionToHandle){PooledConnectionHandler.processRequest(connectionToHandle);}
publicvoidsetUpHandlers(){for(inti=0;i
newThread(currentHandler,"Handler"+i).start();}}publicstaticvoidmain(Stringargs[]){PooledRemoteFileServerserver=newPooledRemoteFileServer(1001,3);
server.setUpHandlers();
server.acceptConnections();}}

请注意一下您现在应该熟悉了的import语句。我们给类以下实例变量以保存:

"我们的服务器能同时处理的活动客户机连接的最大数目

"进入的连接的侦听端口(我们没有指定缺省值,但如果您想这样做,并不会受到限制)

"将接受客户机连接请求的ServerSocket

类的构造器用的参数是侦听端口和连接的最大数目

我们的类有一个main()方法和三个其它方法。稍后我们将探究这些方法的细节。现在只须知道setUpHandlers()创建数目为maxConnections的大量PooledConnectionHandler,而其它两个方法则与我们前面已经看到的相似:acceptConnections()在ServerSocket上侦听传入的客户机连接,而handleConnection则在客户机连接一旦被建立后就实际处理它。

实现main()

这里我们实现需作改动的main()方法,该方法将创建能够处理给定数目的客户机连接的PooledRemoteFileServer,并告诉它接受连接:

publicstaticvoidmain(Stringargs[]){PooledRemoteFileServerserver=newPooledRemoteFileServer(1001,3);
server.setUpHandlers();server.acceptConnections();}

我们的main()方法很简单。我们实例化一个新的PooledRemoteFileServer,它将通过调用setUpHandlers()来建立三个PooledConnectionHandler。一旦服务器就绪,我们就告诉它acceptConnections()。

建立连接处理程序

publicvoidsetUpHandlers(){for(inti=0;i<maxConnections;i++){PooledConnectionHandlercurrentHandler=newPooledConnectionHandler();
newThread(currentHandler,"Handler"+i).start();}}

setUpHandlers()方法创建maxConnections(例如3)个PooledConnectionHandler并在新Thread中激活它们。用实现了Runnable的对象来创建Thread使我们可以在Thread调用start()并且可以期望在Runnable上调用了run()。换句话说,我们的PooledConnectionHandler将等着处理进入的连接,每个都在它自己的Thread中进行。我们在示例中只创建三个Thread,而且一旦服务器运行,这就不能被改变。

处理连接

这里我们实现需作改动的handleConnections()方法,它将委派PooledConnectionHandler处理连接:

protectedvoidhandleConnection(SocketconnectionToHandle){PooledConnectionHandler.processRequest(connectionToHandle);}

我们现在叫PooledConnectionHandler处理所有进入的连接(processRequest()是一个静态方法)。

创建PooledRemoteFileServer类

importjava.io.*;
importjava.net.*;
importjava.util.*;
publicclassPooledConnectionHandlerimplementsRunnable{protectedSocketconnection;
protectedstaticListpool=newLinkedList();
publicPooledConnectionHandler(){}publicvoidhandleConnection(){try{PrintWriterstreamWriter=newPrintWriter(connection.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(connection.getInputStream()));
StringfileToRead=streamReader.readLine();
BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=null;
while((line=fileReader.readLine())!=null)streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();}
catch(FileNotFoundExceptione){System.out.println("");}catch(IOExceptione){System.out.println(""+e);}}
publicstaticvoidprocessRequest(SocketrequestToHandle){synchronized(pool){pool.add(pool.size(),requestToHandle);pool.notifyAll();}}
publicvoidrun(){while(true){synchronized(pool){while(pool.isEmpty()){try{pool.wait();}
catch(InterruptedExceptione){e.printStackTrace();}}connection=(Socket)pool.remove(0);}
handleConnection();}}}

这个助手类与ConnectionHandler非常相似,但它带有处理连接池的手段。该类有两个实例变量:

"connection是当前正在处理的Socket

"名为pool的静态LinkedList保存需被处理的连接

填充连接池

这里我们实现PooledConnectionHandler上的processRequest()方法,它将把传入请求添加到池中,并告诉其它正在等待的对象该池已经有一些内容:

publicstaticvoidprocessRequest(SocketrequestToHandle){synchronized(pool){pool.add(pool.size(),requestToHandle);
pool.notifyAll();}}

synchronized块是个稍微有些不同的东西。您可以同步任何对象上的一个块,而不只是在本身的某个方法中含有该块的对象。在我们的示例中,processRequest()方法包含有一个pool(请记住它是一个LinkedList,保存等待处理的连接池)的synchronized块。我们这样做的原因是确保没有别人能跟我们同时修改连接池。

既然我们已经保证了我们是唯一“涉水”池中的人,我们就可以把传入的Socket添加到LinkedList的尾端。一旦我们添加了新的连接,我们就用以下代码通知其它正在等待该池的Thread,池现在已经可用:

pool.notifyAll();

Object的所有子类都继承这个notifyAll()方法。这个方法,连同我们下一屏将要讨论的wait()方法一起,就使一个Thread能够让另一个Thread知道一些条件已经具备。这意味着该第二个Thread一定正在等待那些条件的满足。

从池中获取连接

这里我们实现PooledConnectionHandler上需作改动的run()方法,它将在连接池上等待,并且池中一有连接就处理它:
publicvoidrun(){while(true){synchronized(pool){while(pool.isEmpty()){try{pool.wait();}
catch(InterruptedExceptione){e.printStackTrace();}}
connection=(Socket)pool.remove(0);}
handleConnection();}}

回想一下在前面讲过的:一个Thread正在等待有人通知它连接池方面的条件已经满足了。在我们的示例中,请记住我们有三个PooledConnectionHandler在等待使用池中的连接。每个PooledConnectionHandler都在它自已的Thread中运行,并通过调用pool.wait()产生阻塞。当我们的processRequest()在连接池上调用notifyAll()时,所有正在等待的PooledConnectionHandler都将得到“池已经可用”的通知。然后各自继续前行调用pool.wait(),并重新检查while(pool.isEmpty())循环条件。除了一个处理程序,其它池对所有处理程序都将是空的,因此,在调用pool.wait()时,除了一个处理程序,其它所有处理程序都将再次产生阻塞。恰巧碰上非空池的处理程序将跳出while(pool.isEmpty())循环并攫取池中的第一个连接:

connection=(Socket)pool.remove(0);

处理程序一旦有一个连接可以使用,就调用handleConnection()处理它。

在我们的示例中,池中可能永远不会有多个连接,只是因为事情很快就被处理掉了。如果池中有一个以上连接,那么其它处理程序将不必等待新的连接被添加到池。当它们检查pool.isEmpty()条件时,将发现其值为假,然后就从池中攫取一个连接并处理它。

还有另一件事需注意。当run()拥有池的互斥锁时,processRequest()如何能够把连接放到池中呢?答案是对池上的wait()的调用释放锁,而wait()接着就在自己返回之前再次攫取该锁。这就使得池对象的其它同步代码可以获取该锁。

处理连接:再一次

这里我们实现需做改动的handleConnection()方法,该方法将攫取连接的流,使用它们,并在任务完成之后清除它们:

publicvoidhandleConnection()
{try{PrintWriterstreamWriter=newPrintWriter(connection.getOutputStream());
BufferedReaderstreamReader=newBufferedReader(newInputStreamReader(connection.getInputStream()));
StringfileToRead=streamReader.readLine();
BufferedReaderfileReader=newBufferedReader(newFileReader(fileToRead));
Stringline=null;while((line=fileReader.readLine())!=null)streamWriter.println(line);
fileReader.close();
streamWriter.close();
streamReader.close();}
catch(FileNotFoundExceptione){System.out.println("");}
catch(IOExceptione){System.out.println(""+e);}}

跟在多线程服务器中不同,我们的PooledConnectionHandler有一个handleConnection()方法。这个方法的代码跟非池式的ConnectionHandler上的run()方法的代码完全一样。首先,我们把OutputStream和InputStream分别包装进(用Socket上的getOutputStream()和getInputStream())BufferedReader和PrintWriter。然后我们逐行读目标文件,就象我们在多线程示例中做的那样。再一次,我们获取一些字节之后就把它们放到本地的line变量中,然后写出到客户机。完成读写操作之后,我们关闭FileReader和打开的流。

总结一下带有连接池的服务器

让我们回顾一下创建和使用“池版”服务器的步骤:

1.创建一个新种类的连接处理程序(我们称之为PooledConnectionHandler)来处理池中的连接。

2.修改服务器以创建和使用一组PooledConnectionHandler。

Java语言简化了套接字在应用程序中的使用。它的基础实际上是java.net包中的Socket和ServerSocket类。一旦您理解了表象背后发生的情况,就能容易地使用这些类。在现实生活中使用套接字只是这样一件事,即通过贯彻优秀的OO设计原则来保护应用程序中各层间的封装。我们为您展示了一些有帮助的类。这些类的结构对我们的应用程序隐藏了Socket交互作用的低级细节?使应用程序能只使用可插入的ClientSocketFacade和ServerSocketFacade。在有些地方(在Facade内),您仍然必须管理稍显杂乱的字节细节,但您只须做一次就可以了。更好的是,您可以在将来的项目中重用这些低级别的助手类。

分享到:
评论

相关推荐

    Java套接字网络编程详解.pdf

    Java套接字网络编程详解 Java套接字网络编程是一种强大的技术,用于开发基于TCP/IP网络的Java程序。Java的这种力量来源于他独有的用于网络的API,这些API是一系列的类和接口,均位于包java.net和javax.net中。 套...

    Java 套接字编程Java 套接字编程

    ### Java套接字编程详解 #### 一、引言 随着互联网技术的不断发展与普及,网络编程成为现代软件开发中不可或缺的一部分。Java作为一种广泛应用的编程语言,提供了丰富的API支持网络编程,尤其是对于套接字(Socket)...

    Java套接字编程

    ### Java套接字编程详解 #### 一、Java网络编程概览 Java作为一种全面支持网络功能的编程语言,其强大的网络编程能力主要归功于它的一系列网络API,特别是位于`java.net`和`javax.net`包中的API。这些API为开发者...

    java套接字编程java套接字编程.doc

    "Java套接字编程详解" Java套接字编程是Java开发网络软件的核心技术之一,利用Java提供的强大网络API,可以轻松地开发网络通信软件。在这篇文章中,我们将详细介绍套接字(Socket)的概念,并通过实例说明如何使用...

    套接字详解(自己找的好资料)

    1. 流套接字(Socket与ServerSocket):Java中的`java.net.Socket`类代表客户端套接字,`java.net.ServerSocket`类用于服务器端。客户端通过`Socket`类的构造函数建立到服务器的连接,服务器端通过`ServerSocket`的`...

    Java网络socket编程详解

    在7.2章节“面向套接字编程”中,描述了如何使用Socket来实现基于TCP协议的服务器和客户机程序。 对于客户端,其操作步骤如下: 1. 使用服务器的IP地址和端口号实例化Socket对象。 2. 调用Socket的connect方法,...

    Java使用socket网络编程详解

    #### 三、面向套接字编程 ##### 3.1 基于TCP协议的服务器和客户机程序概述 根据TCP协议,在客户端-服务器架构中,客户端和服务器的Socket交互流程如下: - **客户端**: 1. 使用服务器的IP地址和端口号实例化`...

    java编程-socket-套接字-网络编程.docx

    - **Socket**:在网络编程中,Socket(套接字)是一种用于网络通信的端点抽象,它提供了一种机制,允许两个或多个应用程序通过网络进行通信。 - **ServerSocket**:服务器端的Socket,用于监听特定端口上的连接请求...

    Java网络socket编程详解1.docx

    Socket,又称为套接字,是网络编程中两个进程间通信的一种端点。在TCP/IP模型中,Socket用于在不同主机间的进程之间建立可靠的、双向的数据传输通道。TCP协议提供面向连接的服务,确保数据的可靠传输,而Socket则是...

    简单的套接字编程,讲的很易懂

    ### 套接字编程基础知识点详解 #### 1.1 什么是套接字 套接字(Socket),作为同一台主机内部应用层与传输层之间的接口,扮演着应用程序和网络之间的桥梁角色,即应用程序接口(API)。它允许应用程序通过网络进行...

    Java软件开发实战 Java基础与案例开发详解 19-6 Datagram套接字 共8页.pdf

    根据给定文件的信息,我们可以提炼出关于Java软件开发中涉及Datagram套接字的重要知识点,以及相关的背景知识。以下是对这些知识点的详细解读: ### 1. Java基础概述 #### 1.1 什么是Java Java是一种广泛使用的...

    Java Socket编程.pdf

    在Java中,套接字分为两种类型:TCP套接字和UDP套接字。TCP套接字基于TCP协议,适用于可靠的连接,因为它提供了数据传输的顺序保证和流量控制。UDP套接字基于UDP协议,适用于不需要保证传输顺序和完整性的场景,如...

    Java使用TCP套接字实现多人聊天功能详解

    Java使用TCP套接字实现多人聊天功能详解 Java使用TCP套接字实现多人聊天功能是指通过Java语言使用TCP套接字来实现多人聊天的功能。这种功能可以使多个用户同时在线聊天,实现实时通讯。 首先,需要了解TCP套接字...

    Java网络socket编程详解.doc

    本文将深入讲解Java套接字(Socket)编程,并通过实例演示如何构建基于TCP协议的服务器和客户端程序。 首先,TCP是一种面向连接的、可靠的传输层协议,确保了数据的完整性和顺序性。在Java中,Socket类代表了TCP...

    局域网聊天程序(xml保存)(异步套接字技术)(只要一分,两个都一样).rar

    《局域网聊天程序:基于XML保存与异步套接字技术详解》 在计算机网络编程领域,局域网聊天程序是一种常见的应用,它允许在同一网络内的设备进行实时通信。本篇文章将深入探讨如何利用XML进行数据存储以及异步套接字...

    socket详解

    Socket,通常被称为套接字,是网络编程中的基本组件,它在客户端和服务器之间建立起通信的桥梁。本文将深入解析Socket类的构造方法、成员方法以及相关选项,帮助理解如何在Java中使用Socket进行网络通信。 **一、...

    Java TCP/IP Socket编程

    - **UDP套接字**:UDP(用户数据报协议)是一种无连接的传输层协议,它不保证数据的可靠传输,但具有较高的传输效率。 - **DatagramPacket类**:用于封装发送和接收的数据包。 - **UDP客户端和服务器端**:通过...

Global site tag (gtag.js) - Google Analytics