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

什么是套接字(Socket)?


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

java 套接字发送日志_socket

我们来分析一下图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,下面我们就来谈谈两种套接字:流套接字和自寻址数据套 接字。

流套接字(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中。

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

由于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

// 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地址的输出。

Socket类
当客户程序需要与服务器程序通讯的时候,客户程序在客户机创建一个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对象。

如果创建了一个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;

 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);

  // 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 ());

 }

 catch (IOException e)

 {

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

 }

 finally

 {

  try

  {

 if (br != null)

  br.close ();

 if (pw != null)

  pw.close ();

 if (s != null)

  s.close ();

  }

  catch (IOException e)

  {

 }

 }

  }

 }

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

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 SSClientwww.sina.com.cn。