- 一、客户机/服务器模式 在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model)。该模式的建立基于以下两点:1、非对等作用;2、通信完全是异步的。客户机/服务器模式在操作过程中采取的是主动请示方式:
首先服务器方要先启动,并根据请示提供相应服务:(过程如下) 1、打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。 2、等待客户请求到达该端口。 3、接收到重复服务请求,处理该请求并发送应答信号。 4、返回第二步,等待另一客户请求 5、关闭服务器。 客户方: 1、打开一通信通道,并连接到服务器所在主机的特定端口。 2、向服务器发送服务请求报文,等待并接收应答;继续提出请求…… 3、请求结束后关闭通信通道并终止。
二、基本套接字 为了更好说明套接字编程原理,给出几个基本的套接字,在以后的篇幅中会给出更详细的使用说明。 1、创建套接字——socket() 功能:使用前创建一个新的套接字 格式:SOCKET PASCAL FAR socket(int af,int type,int procotol); 参数:af: 通信发生的区域 type: 要建立的套接字类型 procotol: 使用的特定协议
2、指定本地地址——bind() 功能:将套接字地址与所创建的套接字号联系起来。 格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen); 参数:s: 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。 其它:没有错误,bind()返回0,否则SOCKET_ERROR 地址结构说明: struct sockaddr_in { short sin_family;//AF_INET u_short sin_port;//16位端口号,网络字节顺序 struct in_addr sin_addr;//32位IP地址,网络字节顺序 char sin_zero[8];//保留 }
3、建立套接字连接——connect()和accept() 功能:共同完成连接工作 格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen); SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen); 参数:同上
4、监听连接——listen() 功能:用于面向连接服务器,表明它愿意接收连接。 格式:int PASCAL FAR listen(SOCKET s, int backlog);
5、数据传输——send()与recv() 功能:数据的发送与接收 格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags); int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags); 参数:buf:指向存有传输数据的缓冲区的指针。 -
6、多路复用——select() 功能:用来检测一个或多个套接字状态。 格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds, fd_set FAR * exceptfds,const struct timeval FAR * timeout); 参数:readfds:指向要做读检测的指针 writefds:指向要做写检测的指针 exceptfds:指向要检测是否出错的指针 timeout:最大等待时间
7、关闭套接字——closesocket() 功能:关闭套接字s 格式:BOOL PASCAL FAR closesocket(SOCKET s);
Windows Socket 套接字原理
一、客户机/服务器模式
在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model)。该模式的建立基于以下两点:1、非对等作用;2、通信完全是异步的。客户机/服务器模式在操作过程中采取的是主动请示方式:
首先服务器方要先启动,并根据请示提供相应服务:(过程如下)
1、打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。
2、等待客户请求到达该端口。
3、接收到重复服务请求,处理该请求并发送应答信号。
4、返回第二步,等待另一客户请求
5、关闭服务器。
客户方:
1、打开一通信通道,并连接到服务器所在主机的特定端口。
2、向服务器发送服务请求报文,等待并接收应答;继续提出请求……
3、请求结束后关闭通信通道并终止。
二、典型过程图
2.1 面向连接的套接字的系统调用时序图
2.2 无连接协议的套接字调用时序图
2.3 面向连接的应用程序流程图
三、 Socket有五种不同的类型:
1、流式套接字(stream socket)
定义:#define SOCK_STREAM 1
流式套接字提供了双向、有序的、无重复的以及无记录边界的数据流服务,适合处理大量数据。它是面向联结的,必须建立数据传输链路,同时还必须对传输的数据进行验证,确保数据的准确性。因此,系统开销较大。
2、 数据报套接字(datagram socket)
定义:#define SOCK_DGRAM 2
数据报套接字也支持双向的数据流,但不保证传输数据的准确性,但保留了记录边界。由于数据报套接字是无联接的,例如广播时的联接,所以并不保证接收端是否正在侦听。数据报套接字传输效率比较高。
3、原始套接字(raw-protocol interface)
定义:#define SOCK_RAW 3
原始套接字保存了数据包中的完整IP头,前面两种套接字只能收到用户数据。因此可以通过原始套接字对数据进行分析。
◆Socket开发所必须需要的文件(以WinSock V2.0为例):
头文件:Winsock2.h
库文件:WS2_32.LIB
动态库:W32_32.DLL
四、基本套接字
1、创建套接字——socket()
功能:使用前创建一个新的套接字
格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);
参数:af: 通信发生的区域
type: 要建立的套接字类型
procotol: 使用的特定协议
2、指定本地地址——bind()
功能:将套接字地址与所创建的套接字号联系起来。
格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);
参数:s: 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
其它:没有错误,bind()返回0,否则SOCKET_ERROR
地址结构说明:
struct sockaddr_in
{
short sin_family;//AF_INET
u_short sin_port;//16位端口号,网络字节顺序
struct in_addr sin_addr;//32位IP地址,网络字节顺序
char sin_zero[8];//保留
}
3、建立套接字连接——connect()和accept()
功能:共同完成连接工作
格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);
SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);
参数:同上
4、监听连接——listen()
功能:用于面向连接服务器,表明它愿意接收连接。
格式:int PASCAL FAR listen(SOCKET s, int backlog);
5、数据传输——send()与recv()
功能:数据的发送与接收
格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags);
int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);
参数:buf:指向存有传输数据的缓冲区的指针。
6、多路复用——select()
功能:用来检测一个或多个套接字状态。
格式:int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,
fd_set FAR * exceptfds,const struct timeval FAR * timeout);
参数:readfds:指向要做读检测的指针
writefds:指向要做写检测的指针
exceptfds:指向要检测是否出错的指针
timeout:最大等待时间
7、关闭套接字——closesocket()
功能:关闭套接字s
格式:BOOL PASCAL FAR closesocket(SOCKET s);
JAVA Socket 套接字原理
用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我们来分析一下图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 SSClient www.sina.com.cn。
技巧
Socket类包含了许多有用的方法。比如getLocalAddress()将返回一个包含客户程序IP地址的InetAddress子类对象的引用;getLocalPort()将返回客户程序的端口号;getInetAddress()将返回一个包含服务器IP地址的InetAddress子类对象的引用;getPort()将返回服务程序的端口号。ServerSocket类
由于SSClient使用了流套接字,所以服务程序也要使用流套接字。这就要创建一个ServerSocket对象,ServerSocket有几个构造函数,最简单的是ServerSocket(int port),当使用ServerSocket(int port)创建一个ServerSocket对象,port参数传递端口号,这个端口就是服务器监听连接请求的端口,如果在这时出现错误将抛出IOException异常对象,否则将创建ServerSocket对象并开始准备接收连接请求。
接下来服务程序进入无限循环之中,无限循环从调用ServerSocket的accept()方法开始,在调用开始后accept()方法将导致调用线程阻塞直到连接建立。在建立连接后accept()返回一个最近创建的Socket对象,该Socket对象绑定了客户程序的IP地址或端口号。
由于存在单个服务程序与多个客户程序通讯的可能,所以服务程序响应客户程序不应该花很多时间,否则客户程序在得到服务前有可能花很多时间来等待通讯的建立,然而服务程序和客户程序的会话有可能是很长的(这与电话类似),因此为加快对客户程序连接请求的响应,典型的方法是服务器主机运行一个后台线程,这个后台线程处理服务程序和客户程序的通讯。
为了示范我们在上面谈到的慨念并完成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 (); } } }
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.
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; // 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"); 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); } 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 (); } catch (IOException e) { } } } }运行这段程序将得到下面的输出:
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程序。DatagramSocket类
DatagramSocket类在客户端创建自寻址套接字与服务器端进行通信连接,并发送和接受自寻址套接字。虽然有多个构造函数可供选择,但我发现创建客户端自寻址套接字最便利的选择是DatagramSocket()函数,而服务器端则是DatagramSocket(int port)函数,如果未能创建自寻址套接字或绑定自寻址套接字到本地端口,那么这两个函数都将抛出一个SocketException对象,一旦程序创建了DatagramSocket对象,那么程序分别调用send(DatagramPacket dgp)和 receive(DatagramPacket dgp)来发送和接收自寻址数据包,
List4显示的DGSClient源代码示范了如何创建自寻址套接字以及如何通过套接字处理发送和接收信息
Listing 4: DGSClient.java // DGSClient.java
import java.io.*; import java.net.*;
class DGSClient { 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];
DatagramSocket s = null;
try { // Create a datagram socket bound to an arbitrary port.
s = new DatagramSocket ();
// Create a byte array that will hold the data portion of a // datagram packet''s message. That message originates as a // String object, which gets converted to a sequence of // bytes when String''s getBytes() method is called. The // conversion uses the platform''s default character set.
byte [] buffer; buffer = new String ("Send me a datagram").getBytes ();
// Convert the name of the host to an InetAddress object. // That object contains the IP address of the host and is // used by DatagramPacket.
InetAddress ia = InetAddress.getByName (host);
// Create a DatagramPacket object that encapsulates a
// reference to the byte array and destination address // information. The destination address consists of the // host''s IP address (as stored in the InetAddress object) // and port number 10000 -- the port on which the server // program listens.
DatagramPacket dgp = new DatagramPacket (buffer, buffer.length, ia, 10000);
// Send the datagram packet over the socket.
s.send (dgp);
// Create a byte array to hold the response from the server. // program.
byte [] buffer2 = new byte [100];
// Create a DatagramPacket object that specifies a buffer // to hold the server program''s response, the IP address of // the server program''s computer, and port number 10000.
dgp = new DatagramPacket (buffer2, buffer.length, ia, 10000);
// Receive a datagram packet over the socket.
s.receive (dgp);
// Print the data returned from the server program and stored // in the datagram packet.
System.out.println (new String (dgp.getData ()));
} catch (IOException e) { System.out.println (e.toString ()); } finally { if (s != null) s.close (); } } }
DGSClient由创建一个绑定任意本地(客户端)端口好的DatagramSocket对象开始,然后装入带有文本信息的数组buffer和描述服务器主机IP地址的InetAddress子类对象的引用,接下来,DGSClient创建了一个DatagramPacket对象,该对象加入了带文本信息的缓冲器的引用,InetAddress子类对象的引用,以及服务端口号10000, DatagramPacket的自寻址数据包通过方法sent()发送给服务器程序,于是一个包含服务程序响应的新的DatagramPacket对象被创建,receive()得到响应的自寻址数据包,然后自寻址数据包的getData()方法返回该自寻址数据包的一个引用,最后关闭DatagramSocket。
DGSServer服务程序补充了DGSClient的不足,List5是DGSServer的源代码:
Listing 5: DGSServer.java // DGSServer.java
import java.io.*; import java.net.*;
class DGSServer { public static void main (String [] args) throws IOException { System.out.println ("Server starting ...\n");
// Create a datagram socket bound to port 10000. Datagram // packets sent from client programs arrive at this port.
DatagramSocket s = new DatagramSocket (10000);
// Create a byte array to hold data contents of datagram // packet.
byte [] data = new byte [100];
// Create a DatagramPacket object that encapsulates a reference // to the byte array and destination address information. The // DatagramPacket object is not initialized to an address // because it obtains that address from the client program.
DatagramPacket dgp = new DatagramPacket (data, data.length);
// Enter an infinite loop. Press Ctrl+C to terminate program.
while (true) { // Receive a datagram packet from the client program.
s.receive (dgp);
// Display contents of datagram packet.
System.out.println (new String (data));
// Echo datagram packet back to client program.
s.send (dgp); } } }
DGSServer创建了一个绑定端口10000的自寻址套接字,然后创建一个字节数组容纳自寻址信息,并创建自寻址包,下一步,DGSServer进入一个无限循环中以接收自寻址数据包、显示内容并将响应返回客户端,自寻址套接没有关闭,因为循环是无限的。
在编译DGSServer 和DGSClient的源代码后,由输入java DGSServer开始运行DGSServer,然后在同一主机上输入Java DGSClient开始运行DGSClient,如果DGSServer与DGSClient运行于不同主机,在输入时注意要在命令行加上服务程序的主机名或IP地址,如:java DGSClient www.yesky.com
路漫漫其修远兮,吾将上下而求索!