第13章 网络编程
890
2012-6-20

第十三章 Java网络编程

 

 

一、Java套接字实现网络编程之基础篇 1

 

用 Java 开发网络软件非常方便和强大,Java 的这种力量来源于他独有的一套强大的用于 网络的 API,这些 API 是一系列的类和接口,均位于包 java.net 和 javax.net 中。在这篇文 章中我们将介绍套接字(Socket)慨念,同时以实例说明如何使 用 Network API 操纵套接字, 在完成本文后,你将具备编写网络低端通讯软件的能力。

 

 

 

1、什么是套接字(Socket)?

 

Network API 是典型的用于基于 TCP/IP 网络 Java 程序与其他程序通讯,Network API 依靠 Socket 进行通讯。Socket 可以看成在两个程序进行通讯连接中的一个端点,一个程序将 一段信息写入 Socket 中,该 Socket 将这段信息发送给另外一个 Socket 中,使这段信息能传 送到其他程序中。如图 1

 

 

 

 

我们来分析一下图 1,Host A 上的程序 A 将一段信息写入 Socket 中,Socket 的内容被 Host A 的网络管理软件访问,并将这段信息通 过 Host A 的网络接口卡发送到 Host B,Host B 的网络接口卡接收到这段信息后,传送给 Host B 的网络管理软件,网络管理软件将这段信息 保 存在 Host B 的 Socket 中,然后程序 B 才能在 Socket 中阅读这段信息。

 

假设在图 1 的网络中添加第三个主机 Host C,那么 Host A 怎么知道信息被正确传送到

Host B 而不是被传送到 Host C 中了呢?基于 TCP/IP 网络中的每一个主机均被赋予了一个唯 一的 IP 地址,IP 地址是一个 32 位的无符号整数,由于没有转变成二进制,因此通常以小数点

 


分隔,如:198.163.227.6,正如所见 IP 地址均由四个部分组成,每个部分的范围都是 0-255, 以表示 8 位地址。

 

值得注意的是 IP 地址都是 32 位地址,这是 IP 协议版本 4(简称 Ipv4)规定的,目前由

于 IPv4 地址已近耗尽,所以 IPv6 地址正逐渐代替 Ipv4 地址,Ipv6 地址则是 128 位无符号 整数。

 

假设第二个程序被加入图 1 的网络的 Host B 中,那么由 Host A 传来的信息如何能被正确 的传给程序 B 而不是传给新加入的程序呢?这是因为每一个基于 TCP/IP 网络通讯的程序都被 赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留 Socket 中的输入/输出信息, 端口号是一个 16 位无符号整数,范围是 0-65535,以区别主机上的每一个程序(端口号就像 房屋中的房间号),低于 256 的短口号保留给标准应用程序,比如 pop3 的端口号就是 110, 每一个套接字都组合进了 IP 地址、端口、端口号,这样形成的整体就可以区别每一个套接字 t, 下面我们就来谈谈两种套接字:流套接字和自寻址数据套接字。

2、流套接字(Stream Socket) 无论何时,在两个网络应用程序之间发送和接收信息时都需要建立一个可靠的连接,流套

接字依靠 TCP 协议来保证信息正确到达目的地,实际上,IP 包有可能在网络中丢失或者在传送

过程中发生错误,任何一种情况发生,作为接受方的 TCP 将联系发送方 TCP 重新发送这个 IP

包。这就是所谓的在两个流套接字之间建立可靠的连接。

 

流套接字在 C/S 程序中扮演一个必需的角色,客户机程序(需要访问某些服务的网络应用 程序)创建一个扮演服务器程序的主机的 IP 地址和服务器程序(为客户端应用程序提供服务的 网络应用程序)的端口号的流套接字对象。

 

客户端流套接字的初始化代码将 IP 地址和端口号传递给客户端主机的网络管理软件,管理 软件将 IP 地址和端口号通过 NIC 传递给服务器端主机;服务器端主机读到经过 NIC 传递来的 数据,然后查看服务器程序是否处于监听状态,这种监听依然是通过套接字和端口来进行的;如 果服务器程序处于监听状态,那么服务器端网络管理软件就向客户机网络管理软件发出一个积极 的响应信号,接收到响应信号后,客户端流套接字初始化代码就给客户程序建立一个端口号,并 将这个端口号传递给服务器程序的套接字(服务器程序将使用这个端口号识别传来的信息是否是 属于客户程序)同时完成流套接字的初始化。

 

如果服务器程序没有处于监听状态,那么服务器端网络管理软件将给客户端传递一个消极 信号,收到这个消极信号后,客户程序的流套接字初始化代码将抛出一个异常对象并且不建立通 讯连接,也不创建流套接字对象。这种情形就像打电话一样,当有人的时候通讯建立,否则电话 将被挂起。

 

这部分的工作包括了相关联的三个类:

InetAddress, Socket, 和 ServerSocket。 InetAddress 对象描绘了 32 位或 128 位 IP 地 址,Socket 对象代表了客户程序流套接字,ServerSocket 代表了服务程序流套接字,所有这 三个类均位于包 java.net 中。

 

3InetAddress

 

InetAddress 类在网络 API 套接字编程中扮演了一个重要角色。参数传递给流套接字类和 自寻址套接字类构造器或非构造器方法。 InetAddress 描述了 32 位或 64 位 IP 地址,要完成 这个功能,InetAddress 类主要依靠两个支持类 Inet4Address 和  Inet6Address,这三个 类是继承关系,InetAddrress 是父类,Inet4Address 和 Inet6Address 是子类。

 

2


由于 InetAddress 类只有一个构造函数,而且不能传递参数,所以不能直接创建

InetAddress 对象,比如下面的做法就是错误的:

 

 

InetAddress ia = new InetAddress ();

 

 

但我们可以通过下面的 5 个工厂方法创建来创建一个 InetAddress 对象或 InetAddress 数组:

 

. getAllByName(String host)方法返回一个 InetAddress 对象的引用,每个对象包含一个 表示相应主机名的单独的 IP 地址,这个 IP 地址是通过 host 参数传递的,对于指定的主机如果 没有 IP 地址存在那么这个方法将抛出一个 UnknownHostException 异常对象。

 

. getByAddress(byte [] addr)方法返回一个 InetAddress 对象的引用,这个对象包含了 一个 Ipv4 地址或 Ipv6 地址,Ipv4 地址是一个 4 字节数组,Ipv6 地址是一个 16 字节地址数 组,如果返回的数组既不是 4 字节的也不是 16 字节的,那么方法将会抛出一个 UnknownHostException 异常对象。

 

. getByAddress(String host, byte [] addr)方法返回一个 InetAddress 对象的引用,这

个 InetAddress 对象包含了一个由 host 和 4 字节的 addr 数组指定的 IP 地址,或者是 host

和 16 字节的 addr 数组指定的 IP 地址,如果这 个数组既不是 4 字节的也不是 16 位字节的, 那么该方法将抛出一个 UnknownHostException 异常对象。

 

. getByName(String host)方法返回一个 InetAddress 对象,该对象包含了一个与 host

参数指定的主机相对应的 IP 地址,对于指定的主机如果没有 IP 地址存在,那么方法将抛出一

个 UnknownHostException 异常对象。

 

. getLocalHost()方法返回一个 InetAddress 对象,这个对象包含了本地机的 IP 地址,考 虑到本地主机既是客户程序主机又是服务器程序主机,为避免混乱,我们将客户程序主机称为客 户主机,将服务器程序主机称为服务器主机。

 

上面讲到的方法均提到返回一个或多个 InetAddress 对象的引用,实际上每一个方法都要 返回一个或多个 Inet4Address/Inet6Address 对象的引用,调用者不需要知道引用的子类型, 相反调用者可以使用返回的引用调用 InetAddress 对象的非静态方法,包括子类型的多态以确 保重载方法被调用。

 

InetAddress 和它的子类型对象处理主机名到主机 IPv4 或 IPv6 地址的转换,要完成这个 转换需要使用域名系统,下面的代码示范了如何通过调用 getByName(String host)方法获得 InetAddress 子类对象的方法,这个对象包含了与 host 参数相对应的 IP 地址:

 

 

InetAddress ia = InetAddress.getByName ("www.javajeff.com"));

 

 

一但获得了 InetAddress 子类对象的引用就可以调用 InetAddress 的各种方法来获得

InetAddress 子类对象中的 IP 地址信 息,比如,可以通过调用 getCanonicalHostName() 从域名服务中获得标准的主机名;getHostAddress()获得 IP 地址, getHostName()获得主 机名,isLoopbackAddress()判断 IP 地址是否是一个 loopback 地址。

 

List1 是一段示范代码:InetAddressDemo

 

 

 

 

3


// InetAddressDemo.java

import java.net.*;

class InetAddressDemo

{

public static void main (String [] args) throws UnknownHostException

{

String host = "localhost";

if (args.length == 1)

 

host = args [0];

 

InetAddress ia = InetAddress.getByName (host);

 

System.out.println ("Canonical Host Name = " +ia.getCanonicalHostName

());

System.out.println ("Host Address = " +ia.getHostAddress ());

System.out.println ("Host Name = " +ia.getHostName ());

System.out.println ("Is Loopback Address = " +ia.isLoopbackAddress ());

}

}

 

当无命令行参数时,代码输出类似下面的结果:

 

Canonical Host Name = localhost

Host Address = 127.0.0.1

Host Name = localhost

Is Loopback Address = true

 

InetAddressDemo 给了你一个指定主机名作为命令行参数的选择,如果没有主机名被指定,

那么将使用 localhost(客户机的), InetAddressDemo 通过调用 getByName(String host) 方法获得一个 InetAddress 子类对象的引用,通过这个引用 获得了标准主机名,主机地址, 主机名以及 IP 地址是否是 loopback 地址的输出。

 

4Socket

 

当客户程序需要与服务器程序通讯的时候,客户程序在客户机创建一个 socket 对象,

Socket 类有几个构造函数。两个常用的构造函数

是 Socket(InetAddress addr, int port) 和 Socket(String host, int port),两个构造函数 都创建了一个基于 Socket 的连接服务器端流套接字的流套接字。对于第一个 InetAddress 子 类对象通过 addr 参数获得服务器主机的 IP 地址,对于第二个函数 host 参数包被分配到 InetAddress 对象中,如果没有 IP 地址与 host 参数相一致,那么将抛出 UnknownHostException 异常对象。两个函数都通过参数 port 获得服务器的端口号。假设已 经建立连接了,网络 API 将在客户端基于 Socket 的流套接字中捆绑客户程序的 IP 地址和任意 一个端口号,否则两个函数都会抛出一个 IOException 对象。

 

 

 

 

 

4


如果创建了一个 Socket 对象,那么它可能通过调用 Socket 的 getInputStream()方法 从服务程序获得输入流读传送来的信息,也可能通过调用 Socket 的 getOutputStream()方法 获得输出流来发送消息。在读写活动完成之后,客户程序调用 close()方法关闭流和流套接字, 下面的代码创建了一个服务程序主机地址为 198.163.227.6,端口号为 13 的 Socket 对象, 然后从这个新创建的 Socket 对象中读取输入流,然后再关闭流和 Socket 对象。

 

Socket s = new Socket ("198.163.227.6", 13);

InputStream is = s.getInputStream ();

// Read from the stream.

is.close ();

s.close ();

 

接下面我们将示范一个流套接字的客户程序,这个程序将创建一个 Socket 对象,Socket

将访问运行在指定主机端口 10000 上的服务程序,如果访问成功客户程序将给服务程序发送一 系列命令并打印服务程序的响应。List2 是我们创建的程序 SSClient 的源代码:

Listing 2:  SSClient.java

 

// SSClient.java

 

import java.io.*;

import java.net.*;

 

class SSClient

{

 

public static void main (String [] args)

 

{

 

String host = "localhost";

 

// If user specifies a command-line argument, that argument

 

// represents the host name.

 

if (args.length == 1)

 

host = args [0];

 

BufferedReader br = null;

 

PrintWriter pw = null;

 

Socket s = null;

 

 

 

5


try

 

{

 

// Create a socket that attempts to connect to the server

 

// program on the host at port 10000.

 

s = new Socket (host, 10000);

 

// Create an input stream reader that chains to the socket's

 

// byte-oriented input stream. The input stream reader

 

// converts bytes read from the socket to characters. The

 

// conversion is based on the platform's default character

 

// set.

 

InputStreamReader isr;

 

isr = new InputStreamReader (s.getInputStream ());

 

// Create a buffered reader that chains to the input stream

 

// reader. The buffered reader supplies a convenient method

 

// for reading entire lines of text.

 

br = new BufferedReader (isr);

 

// Create a print writer that chains to the socket's byte-

 

// oriented output stream. The print writer creates an

 

// intermediate output stream writer that converts

 

// characters sent to the socket to bytes. The conversion

 

// is based on the platform's default character set.

 

pw = new PrintWriter (s.getOutputStream (), true);

 

 

 

6


// Send the DATE command to the server.

 

pw.println ("DATE");

 

// Obtain and print the current date/time.

 

System.out.println (br.readLine ());

 

 

// Send the PAUSE command to the server. This allows several

 

// clients to start and verifies that the server is spawning

 

// multiple threads.

 

pw.println ("PAUSE");

 

// Send the DOW command to the server.

 

pw.println ("DOW");

 

// Obtain and print the current day of week.

 

System.out.println (br.readLine ());

 

// Send the DOM command to the server.

 

 

 

pw.println ("DOM");

 

// Obtain and print the current day of month.

 

System.out.println (br.readLine ());

 

// Send the DOY command to the server.

 

pw.println ("DOY");

 

// Obtain and print the current day of year.

 

System.out.println (br.readLine ());

 

}

 

 

7


 

运行这段程序将会得到下面的结果:

 

Tue Jan 29 18:11:51 CST 2002

 

TUESDAY

 

29

 

29


SSClient 创建了一个 Socket 对象与运行在主机端口 10000 的服务程序联系,主机的 IP

地址由 host 变量确定。SSClient 将获得 Socket 的输入输出流,围绕 BufferedReader 的输 入流和 PrintWriter 的输出流对字符串进行读写操作就变得非常容易,SSClient 向服务程序发 出各种 date/time 命令并得到响应,每个响应均被打印,一旦最后一个响应被打印,将执行 Try/Catch/Finally 结构的 Finally 子串,Finally 子串将在关闭 Socket 之前关闭 BufferedReader 和  PrintWriter。

 

在 SSClient 源代码编译完成后,可以输入 java SSClient 来执行这段程序,如果有合适 的程序运行在不同的主机上,采用主机名/IP 地址 为参数的输入方式,比如 www.sina.com.cn 是运行服务器程序的主机,那么输入方式就是 java SSClient www.sina.com.cn。

 

二、Java套接字实现网络编程之基础篇 2

 

 

1、技巧

Socket 类包含了许多有用的方法。比如 getLocalAddress()将返回一个包含客户程序 IP 地址的 InetAddress 子类对象的引用;getLocalPort()将返回客户程序的端口 号;getInetAddress()将返回一个包含服务器 IP 地址的 InetAddress 子类对象的引用; getPort()将返回服务程序的端口号。

2ServerSocket

 

由于 SSClient 使用了流套接字,所以服务程序也要使用流套接字。这就要创建一个 ServerSocket 对象,ServerSocket 有几个构造函数,最简单的是 ServerSocket(int port), 当使用 ServerSocket (int port)创建一个 ServerSocket 对象,port 参数传递端口号,这个 端口就是服务器监听连接请求的端口,如果在这时出现错误将抛出 IOException 异常对象,否 则将创建 ServerSocket 对象并开始准备接收连接请求。

 

接下来服务程序进入无限循环之中,无限循环从调用 ServerSocket 的 accept()方法开始, 在调用开始后 accept()方法将导致调用线程阻塞直到连接建立。在建立连接后 accept()返回一 个最近创建的 Socket 对象,该 Socket 对象绑定了客户程序的 IP 地址或端口号。

 

由于存在单个服务程序与多个客户程序通讯的可能,所以服务程序响应客户程序不应该花 很多时间,否则客户程序在得到服务前有可能花很多时间来等待通讯的建立,然而服务程序和客 户程序的会话有可能是很长的(这与电话类似),因此为加快对客户程序连接请求的响应,典型 的方法是服务器主机运行一个后台线程,这个后台线程处理服务程序和客户程序的通讯。

 

 

 

 

9


为了示范我们在上面谈到的慨念并完成 SSClient 程序,下面我们创建一个 SSServer 程序, 程序将创建一个 ServerSocket 对象来监听端口 10000 的连接请求,如果成功服务程序将等待 连接输入,开始一个线程处理连接,并响应来自客户程序的命令。下面就是这段程序的代码:

Listing 3: SSServer.java

 

// SSServer.java

 

import java.io.*;

import java.net.*;

import java.util.*;

 

class SSServer

{

 

public static void main (String [] args) throws IOException

 

{

 

System.out.println ("Server starting...\n");

 

// Create a server socket that listens for incoming connection

 

// requests on port 10000.

 

ServerSocket server = new ServerSocket (10000);

 

while (true)

 

{

 

// Listen for incoming connection requests from client

 

// programs, establish a connection, and return a Socket

 

// object that represents this connection.

 

Socket s = server.accept ();

 

System.out.println ("Accepting Connection...\n");

 

// Start a thread to handle the connection.

 

new ServerThread (s).start ();

 

 

 

 

10


}

 

}

 

}

 

 

class ServerThread extends Thread

{

 

private Socket s;

ServerThread (Socket s)

{

this.s = s;

}

 

public void run ()

{

BufferedReader br = null;

PrintWriter pw = null;

 

try

{

 

// Create an input stream reader that chains to the socket's

 

// byte-oriented input stream. The input stream reader

 

// converts bytes read from the socket to characters. The

 

// conversion is based on the platform's default character

 

// set.

 

InputStreamReader isr;

 

isr = new InputStreamReader (s.getInputStream ());

 

// Create a buffered reader that chains to the input stream

 

// reader. The buffered reader supplies a convenient method

 

// for reading entire lines of text.

 

 

 

11


br = new BufferedReader (isr);

 

// Create a print writer that chains to the socket's byte-

 

// oriented output stream. The print writer creates an

 

// intermediate output stream writer that converts

 

// characters sent to the socket to bytes. The conversion

 

// is based on the platform's default character set.

 

pw = new PrintWriter (s.getOutputStream (), true);

 

// Create a calendar that makes it possible to obtain date

 

// and time information.

 

Calendar c = Calendar.getInstance ();

 

 

// Because the client program may send multiple commands, a

 

// loop is required. Keep looping until the client either

 

// explicitly requests termination by sending a command

 

// beginning with letters BYE or implicitly requests

 

// termination by closing its output stream.

 

do

 

{

 

// Obtain the client program's next command.

 

String cmd = br.readLine ();

 

// Exit if client program has closed its output stream.

 

if (cmd == null)

 

break;

 

 

12


 

// Convert command to uppercase, for ease of comparison.

 

cmd = cmd.toUpperCase ();

 

// If client program sends BYE command, terminate.

 

if (cmd.startsWith ("BYE"))

 

break;

 

// If client program sends DATE or TIME command, return

 

// current date/time to the client program.

 

if (cmd.startsWith ("DATE") || cmd.startsWith ("TIME"))

 

pw.println (c.getTime ().toString ());

 

// If client program sends DOM (Day Of Month) command,

 

// return current day of month to the client program.

 

if (cmd.startsWith ("DOM"))

 

pw.println ("" + c.get (Calendar.DAY_OF_MONTH));

 

// If client program sends DOW (Day Of Week) command,

 

// return current weekday (as a string) to the client

 

// program.

 

if (cmd.startsWith ("DOW"))

 

switch (c.get (Calendar.DAY_OF_WEEK))

 

{

 

case Calendar.SUNDAY : pw.println ("SUNDAY");

 

break;

 

case Calendar.MONDAY : pw.println ("MONDAY");

 

 

13


 

break;

 

case Calendar.TUESDAY : pw.println ("TUESDAY");

 

break;

 

case Calendar.WEDNESDAY: pw.println ("WEDNESDAY");

 

break;

 

case Calendar.THURSDAY : pw.println ("THURSDAY");

 

break;

 

case Calendar.FRIDAY : pw.println ("FRIDAY");

 

break;

 

case Calendar.SATURDAY : pw.println ("SATURDAY");

 

}

 

// If client program sends DOY (Day of Year) command,

 

// return current day of year to the client program.

 

if (cmd.startsWith ("DOY"))

 

pw.println ("" + c.get (Calendar.DAY_OF_YEAR));

 

// If client program sends PAUSE command, sleep for three

 

// seconds.

 

 

 

if (cmd.startsWith ("PAUSE"))

 

try

{

 

Thread.sleep (3000);

 

 

 

14


}

 

catch (InterruptedException e)

{

 

}

 

}

 

while (true);

 

{

 

catch (IOException e)

 

{

 

System.out.println (e.toString ());

 

}

 

finally

 

{

 

System.out.println ("Closing Connection...\n");

 

try

 

{

 

if (br != null)

 

br.close ();

 

if (pw != null)

 

pw.close ();

 

if (s != null)

 

s.close ();

 

}

 

 

15


 

运行这段程序将得到下面的输出:

 

Server starting...

 

Accepting Connection...

 

Closing Connection...

 

SSServer 的源代码声明了一对类:SSServer 和 ServerThread;SSServer 的 main()

方法创建了一个 ServerSocket 对象来监听端口 10000 上的连接请求,如果成功, SSServer 进入一个无限循环中,交替调用 ServerSocket 的 accept() 方法来等待连接请求,同时启动 后台线程处理连接(accept()返回的请求)。线程由 ServerThread 继承的 start ()方法开始, 并执行 ServerThread 的 run()方法中的代码。

 

一旦 run()方法运行,线程将创建 BufferedReader, PrintWriter 和 Calendar 对象并进 入一个循环,这个循环由读(通过 BufferedReader 的 readLine())来自客户程序的一行文本 开始,文本(命令)存储在 cmd 引用的 string 对象中,如果客户程序过早的关闭输出流,会发 生什么呢?答案是:cmd 将得不到赋值。

 

注意必须考虑到这种情况:在服务程序正在读输入流时,客户程序关闭了输出流,如果没 有对这种情况进行处理,那么程序将产生异常。

 

一旦编译了 SSServer 的源代码,通过输入 Java SSServer 来运行程序,在开始运行

SSServer 后,就可以运行一个或多个 SSClient 程序。

 

 

三、Java网络编程精解之ServerSocket用法详解一 1

 

 

在客户/服务器通信模式中,服务器端需要创建监听特定端口的 ServerSocket, ServerSocket 负责接收客户连接请求。本章首先介绍 ServerSocket 类的各个构造方法,以 及成员方法的用法,接着介绍服务器如何用多线程来处理与多个客户的通信任务。

 

 

 

 

 

 

16


本章提供线程池的一种实现方式。线程池包括一个工作队列和若干工作线程。服务器程序 向工作队列中加入与客户通信的任务,工作线程不断从工作队列中取出任务并执行它。本章还介 绍了 java.util.concurrent 包中的线程池类的用法,在服务器程序中可以直接使用它们。

 

3.1  构造 ServerSocket

 

ServerSocket 的构造方法有以下几种重载形式:

 

◆ServerSocket()throws IOException

 

◆ServerSocket(int port) throws IOException

 

◆ServerSocket(int port, int backlog) throws IOException

 

◆ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

 

在以上构造方法中,参数 port 指定服务器要绑定的端口(服务器要监听的端口),参数

backlog 指定客户连接请求队列的长度,参数 bindAddr 指定服务器要绑定的 IP 地址。

3.1.1  绑定端口 除了第一个不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端

口由参数 port 指定。例如,以下代码创建了一个与 80 端口绑定的服务器:

 

 

ServerSocket serverSocket=new ServerSocket(80);

 

 

如果运行时无法绑定到 80 端口,以上代码会抛出 IOException,更确切地说,是抛出

BindException,它是 IOException 的子类。BindException 一般是由以下原因造成的:

 

◆端口已经被其他服务器进程占用;

 

◆在某些操作系统中,如果没有以超级用户的身份来运行服务器程序,那么操作系统不允许服务 器绑定到 1~1023 之间的端口。

 

如果把参数 port 设为 0,表示由操作系统来为服务器分配一个任意可用的端口。由操作系 统分配的端口也称为匿名端口。对于多数服务器,会使用明确的端口,而不会使用匿名端口,因 为客户程序需要事先知道服务器的端口,才能方便地访问服务器。在某些场合,匿名端口有着特 殊的用途。

3.1.2  设定客户连接请求队列的长度 当服务器进程运行时,可能会同时监听到多个客户的连接请求。例如,每当一个客户进程

执行以下代码:

 

 

Socket socket=new Socket(www.javathinker.org,80);

 

 

就意味着在远程www.javathinker.org主机的 80 端口上,监听到了一个客户的连接请求。

管理客户连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出 的队列中。许多操作系统限定了队列的最大长度,一般为 50。当队列中的连接请求达到了队列 的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过

 

 

 

17


ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入 新的连接请求。

 

对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器 的连接建立成功,客户进程从 Socket 构造方法中正常返回。如果客户进程发出的连接请求被服 务器拒绝,Socket 构造方法就会抛出 ConnectionException。

 

ServerSocket 构造方法的 backlog 参数用来显式设置连接请求队列的长度,它将覆盖操 作系统限定的队列的最大长度。值得注意的是,在以下几种情况中,仍然会采用操作系统限定的 队列的最大长度:

 

◆backlog 参数的值大于操作系统限定的队列的最大长度;

 

◆backlog 参数的值小于或等于 0;

 

◆在 ServerSocket 构造方法中没有设置 backlog 参数。

 

以下例程 3-1 的 Client.java 和例程 3-2 的 Server.java 用来演示服务器的连接请求队列 的特性。

 

例程 3-1  Client.java

 

 

import java.net.*;

 

public class Client {

 

public static void main(String args[])throws Exception{

final int length=100;

 

String host="localhost";

 

int port=8000;

 

Socket[] sockets=new Socket[length];

 

for(int i=0;i

sockets[i]=new Socket(host, port);

 

 

System.out.println("第"+(i+1)+"次连接成功");

 

 

}

 

 

Thread.sleep(3000);

 

 

for(int i=0;i

sockets[i].close();     //断开连接

 

 

}

 

 

}

 

 

 

 

18


}

 

 

例程 3-2  Server.java

 

 

import java.io.*;

 

import java.net.*;

 

public class Server {

 

private int port=8000;

 

private ServerSocket serverSocket;

 

public Server() throws IOException {

 

serverSocket = new ServerSocket(port,3);  //连接请求队列

的长度为 3

 

System.out.println("服务器启动");

 

}

 

public void service() {

 

while (true) {

 

Socket socket=null;

 

try {

 

socket = serverSocket.accept();   //从连接请求队列中取出

一个连接

 

System.out.println("New connection accepted " +

socket.getInetAddress() + ":" +socket.getPort());

 

 

}catch (IOException e) {

 

 

e.printStackTrace();

 

 

}finally {

 

 

try{

 

 

if(socket!=null)socket.close();

 

 

}catch (IOException e) {e.printStackTrace();}

 

 

}

 

 

 

 

19


}

 

 

}

 

public static void main(String args[])throws Exception {

 

 

Server server=new Server();

 

 

Thread.sleep(60000*10);    //睡眠 10 分钟

 

 

//server.service();

 

 

}

 

 

}

 

 

Client 试图与 Server 进行 100 次连接。在 Server 类中,把连接请求队列的长度设为 3。

这意味着当队列中有了 3 个连接请求时,如果 Client 再请求连接,就会被 Server 拒绝。下面 按照以下步骤运行 Server 和 Client 程序。

 

(1)把 Server 类的 main()方法中的“server.service();”这行程序代码注释掉。这使得服务 器与 8000 端口绑定后,永远不会执行 serverSocket.accept()方法。这意味着队列中的连接 请求永远不会被取出。先运行 Server 程序,然后再运行 Client 程序,Client 程序的打印结果 如下:

 

 

第 1 次连接成功

 

第 2 次连接成功

 

第 3 次连接成功

Exception in thread "main" java.net.ConnectException: Connection refused: connect

at java.net.PlainSocketImpl.socketConnect(Native Method)

at java.net.PlainSocketImpl.doConnect(Unknown Source)

at java.net.PlainSocketImpl.connectToAddress(Unknown Source)

at java.net.PlainSocketImpl.connect(Unknown Source)

at java.net.SocksSocketImpl.connect(Unknown Source)

at java.net.Socket.connect(Unknown Source)

at java.net.Socket.connect(Unknown Source)

at java.net.Socket.(Unknown Source)

at java.net.Socket.(Unknown Source)

at Client.main(Client.java:10)

 

 

从以上打印结果可以看出,Client 与 Server 在成功地建立了 3 个连接后,就无法再创建其余

的连接了,因为服务器的队列已经满了。

 

(2)把 Server 类的 main()方法按如下方式修改:

 

 

20


public static void main(String args[])throws Exception {

Server server=new Server();

//Thread.sleep(60000*10); //睡眠 10 分钟

server.service();

}

 

 

作了以上修改,服务器与 8 000 端口绑定后,就会在一个 while 循环中不断执行

serverSocket.accept()方法,该方法从队列 中取出连接请求,使得队列能及时腾出空位,以 容纳新的连接请求。先运行 Server 程序,然后再运行 Client 程序,Client 程序的打印结果如 下:

 

 

第 1 次连接成功

第 2 次连接成功

第 3 次连接成功

 

第 100 次连接成功

 

 

从以上打印结果可以看出,此时 Client 能顺利与 Server 建立 100 次连接。

 

3.1.3  设定绑定的 IP 地址

 

如果主机只有一个 IP 地址,那么默认情况下,服务器程序就与该 IP 地址绑定。

ServerSocket 的第 4 个构造方法 ServerSocket

(int port, int backlog, InetAddress bindAddr)有一个 bindAddr 参数,它显式指定服务器 要绑定的 IP 地址,该构造方法适用于具有多 IP 地址的主机。假定一个主机有两个网卡,一个 网卡用于连接到 Internet, IP 地址为 222.67.5.94,还有一个网卡用于连接到本地局域网,

IP 地址为 192.168.3.4。如果服务器仅仅被本地局域网中的客户访问,那么可以按如下方式创

建 ServerSocket:

 

 

ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.

3.4"));

 

 

3.1.4  默认构造方法的作用

 

ServerSocket 有一个不带参数的默认构造方法。通过该方法创建的 ServerSocket 不与 任何端口绑定,接下来还需要通过 bind()方法与特定端口绑定。

 

这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置 ServerSocket

的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。

 

在以下代码中,先把 ServerSocket 的 SO_REUSEADDR 选项设为 true,然后再把它与

8000 端口绑定:

 

 

ServerSocket serverSocket=new ServerSocket();

serverSocket.setReuseAddress(true);    //设置

 

 

21


ServerSocket 的选项

serverSocket.bind(new InetSocketAddress(8000)); //与 8000

端口绑定

 

 

如果把以上程序代码改为:

 

 

ServerSocket serverSocket=new ServerSocket(8000);

 

 

serverSocket.setReuseAddress(true);    //设置

ServerSocket 的选项

 

 

那么 serverSocket.setReuseAddress(true)方法就不起任何作用了,因为

SO_ REUSEADDR 选项必须在服务器绑定端口之前设置才有效。

 

 

 

 

四、Java网络编程精解之ServerSocket用法详解一 2

 

 

3.2  接收和关闭与客户的连接

 

ServerSocket 的 accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与 客户连接的 Socket 对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等 待,直到接收到了连接请求才返回。

 

接下来,服务器从 Socket 对象中获得输入流和输出流,就能与客户交换数据。当服务器正 在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个 IOException 的 子类 SocketException 异常:

 

 

java.net.SocketException: Connection reset by peer

 

 

这只是服务器与单个客户通信中出现的异常,这种异常应该被捕获,使得服务器能继续与

其他客户通信。 以下程序显示了单线程服务器采用的通信流程:

 

 

public void service() {

while (true) {

Socket socket=null;

try {

socket = serverSocket.accept();  //从连接请求队列中取出一

个连接

System.out.println("New connection accepted " +

socket.getInetAddress() + ":" +socket.getPort());

//接收和发送数据

 

 

 

22


 

 

}catch (IOException e) {

 

 

//这只是与单个客户通信时遇到的异常,可能是由于客户端过早断开

连接引起的

 

 

//这种异常不应该中断整个 while 循环

 

 

e.printStackTrace();

 

 

}finally {

 

 

try{

 

 

if(socket!=null)

 

socket.close();   //与一个客户通信结束后,要关闭 Socket

}catch (IOException e) {e.printStackTrace();}

 

 

}

}

}

 

 

与单个客户通信的代码放在一个 try 代码块中,如果遇到异常,该异常被 catch 代码块捕

获。try 代码块后面还有一个 finally 代码块,它保证不管与客户通信正常结束还是异常结束, 最后都会关闭 Socket,断开与这个客户的连接。

 

3.3  关闭 ServerSocket

 

ServerSocket 的 close()方法使服务器释放占用的端口,并且断开与所有客户的连接。当 一个服务器程序运行结束时,即使没有执行 ServerSocket 的 close()方法,操作系统也会释 放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行 ServerSocket 的 close()方法。

 

在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以 显式调用 ServerSocket 的 close()方法。例如,以下代码用于扫描 1~65535 之间的端口号。 如果 ServerSocket 成功创建,意味着该端口未被其他服务器进程绑定,否者说明该端口已经 被其他进程占用:

 

 

for(int port=1;port<=65535;port++){

 

 

try{

ServerSocket serverSocket=new ServerSocket(port);

serverSocket.close();  //及时关闭 ServerSocket

}catch(IOException e){

 

 

23


System.out.println("端口"+port+" 已经被其他服务器进程占用

");

 

 

}

 

 

}

 

 

以上程序代码创建了一个 ServerSocket 对象后,就马上关闭它,以便及时释放它占用的

端口,从而避免程序临时占用系统的大多数端口。

 

ServerSocket 的 isClosed()方法判断 ServerSocket 是否关闭,只有执行了 ServerSocket 的 close() 方法,isClosed()方法才返回 true;否则,即使 ServerSocket 还 没有和特定端口绑定,isClosed()方法也会返回 false。

 

ServerSocket 的 isBound()方法判断 ServerSocket 是否已经与一个端口绑定,只要

ServerSocket 已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回 true。

 

如果需要确定一个 ServerSocket 已经与特定端口绑定,并且还没有被关闭,则可以采用 以下方式:

 

 

boolean isOpen=serverSocket.isBound() && !serverSocket.isClosed();

 

 

3.4  获取 ServerSocket 的信息

 

ServerSocket 的以下两个 get 方法可分别获得服务器绑定的 IP 地址,以及绑定的端口:

 

◆public InetAddress getInetAddress()

 

◆public int getLocalPort()

 

前面已经讲到,在构造 ServerSocket 时,如果把端口设为 0,那么将由操作系统为服务 器分配一个端口(称为匿名端口),程序只要调用 getLocalPort()方法就能获知这个端口号。 如例程 3-3 所示的 RandomPort 创建了一个 ServerSocket,它使用的就是匿名端 口。

 

例程 3-4  TimeoutTester.java

 

 

import java.io.*;

 

import java.net.*;

 

public class TimeoutTester{

public static void main(String args[])throws IOException{

ServerSocket serverSocket=new ServerSocket(8000);

serverSocket.setSoTimeout(6000); //等待客户连接的时间不超过

6秒

Socket socket=serverSocket.accept();

socket.close();

System.out.println("服务器关闭");

 

 

 

24


}

}

 

 

运行以上程序,过 6 秒钟后,程序会从 serverSocket.accept()方法中抛出

Socket- TimeoutException:

 

 

C:\chapter03\classes>java TimeoutTester

 

 

Exception in thread "main" java.net.SocketTimeoutException: Accept timed out

at java.net.PlainSocketImpl.socketAccept(Native Method)

at java.net.PlainSocketImpl.accept(Unknown Source)

at java.net.ServerSocket.implAccept(Unknown Source)

at java.net.ServerSocket.accept(Unknown Source)

at TimeoutTester.main(TimeoutTester.java:8)

 

 

如果把程序中的“serverSocket.setSoTimeout(6000)”注释掉,那么

serverSocket. accept()方法永远不会超时,它会一直等待下去,直到接收到了客户的连接, 才会从 accept()方法返回。

 

Tips:服务器执行 serverSocket.accept()方法时,等待客户连接的过程也称为阻塞。

 

 

3.5.2  SO_REUSEADDR 选项

 

◆设置该选项:public void setResuseAddress(boolean on) throws SocketException

 

◆读取该选项:public boolean getResuseAddress() throws SocketException

 

这个选项与 Socket 的 SO_REUSEADDR 选项相同,用于决定如果网络上仍然有数据向旧

的 ServerSocket 传输数据,是否允许新的 ServerSocket 绑定到与旧的 ServerSocket 同样 的端口上。SO_REUSEADDR 选项的默认值与操作系统有关,在某些操作系统中, 允许重用 端口,而在某些操作系统中不允许重用端口。

 

当 ServerSocket 关闭时,如果网络上还有发送到这个 ServerSocket 的数据,这个 ServerSocket 不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的 延迟数据,然后再释放端口。

 

许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一 段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序 无法绑定到该端口,服务器启动失败,并抛出 BindException:

 

 

Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind

 

 

为了确保一个进程关闭了 ServerSocket 后,即使操作系统还没释放端口,同一个主机上的其

他进程还可以立刻重用该端口,可以调用 ServerSocket 的 setResuse- Address(true)方法:

 

 

 

25


if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(true);

 

 

值得注意的是,serverSocket.setResuseAddress(true)方法必须在 ServerSocket 还

没有绑定到一个本地 端口之前调用,否则执行 serverSocket.setResuseAddress(true)方法 无效。此外,两个共用同一个端口的进程必须都调用 serverSocket.setResuseAddress(true) 方法,才能使得一个进程关闭 ServerSocket 后,另一个进程的 ServerSocket 还能够立刻重 用相同端口。