java socket programming

用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程序。

自寻址套接字(datagram sockets)

,因为使用流套接字的每个连接均要花费一定的时间,要减少这种开销,网络api提供了第二种套接字:自寻址套接字(datagram socket),自寻址使用udp发送寻址信息(从客户程序到服务程序或从服务程序到客户程序),不同的是可以通过自寻址套接字发送多ip信息包,自寻址信息包含在自寻址包中,此外自寻址包又包含在ip包内,这就将寻址信息长度限制在60000字节内。图2显示了位于ip包内的自寻址包的自寻址信息。

与tcp保证信息到达信息目的地的方式不同,udp提供了另外一种方法,如果自寻址信息包没有到达目的地,,那么udp也不会请求发送者重新发送自寻址包,这是因为udp在每一个自寻址包中包含了错误检测信息,在每个自寻址包到达目的地之后udp只进行简单的错误检查,如果检测失败,udp将抛弃这个自寻址包,也不会从发送者那里重新请求替代者,这与通过邮局发送信件相似,发信人在发信之前不需要与收信人建立连接,同样也不能保证信件能到达收信人那里

自寻址套接字工作包括下面三个类:datagrampacket, datagramsocket,和 multicastsocket。datagrampacket对象描绘了自寻址包的地址信息,datagramsocket表示客户程序和服务程序自寻址套接字,multicastsocket描绘了能进行多点传送的自寻址套接字,这三个类均位于java.net包内。

datagrampacket类

在使用自寻址包之前,你需要首先熟悉datagrampacket类,地址信息和自寻址包以字节数组的方式同时压缩入这个类创建的对象中

datagrampacket有数个构造函数,即使这些构造函数的形式不同,但通常情况下他们都有两个共同的参数:byte [] buffer 和 int length,buffer参数包含了一个对保存自寻址数据包信息的字节数组的引用,length表示字节数组的长度。

最简单的构造函数是datagrampacket(byte [] buffer, int length),这个构造函数确定了自寻址数据包数组和数组的长度,但没有任何自寻址数据包的地址和端口信息,这些信息可以后面通过调用方法setaddress(inetaddress addr)和setport(int port)添加上,下面的代码示范了这些函数和方法。

byte [] buffer = new byte [100];

datagrampacket dgp = new datagrampacket (buffer, buffer.length);

inetaddress ia = inetaddress.getbyname ("www.disney.com");

dgp.setaddress (ia);

dgp.setport (6000); // send datagram packet to port 6000.

如果你更喜欢在调用构造函数的时候同时包括地址和端口号,可以使用datagrampacket(byte [] buffer, int length, inetaddress addr, int port)函数,下面的代码示范了另外一种选择。

byte [] buffer = new byte [100];

inetaddress ia = inetaddress.getbyname ("www.disney.com");

datagrampacket dgp = new datagrampacket (buffer, buffer.length, ia,

6000);

有时候在创建了datagrampacket对象后想改变字节数组和他的长度,这时可以通过调用setdata(byte [] buffer) 和 setlength(int length)方法来实现。在任何时候都可以通过调用getdata() 来得到字节数组的引用,通过调用getlength()来获得字节数组的长度。下面的代码示范了这些方法:

byte [] buffer2 = new byte [256];

dgp.setdata (buffer2);

dgp.setlength (buffer2.length);

关于datagrampacket的更多信息请参考sdk文档。

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

多点传送和multicastsocket类

前面的例子显示了服务器程序线程发送单一的消息(通过流套接字或自寻址套接字)给唯一的客户端程序,这种行为被称为单点传送(unicasting),多数情况都不适合于单点传送,比如,摇滚歌手举办一场音乐会将通过互联网进行播放,画面和声音的质量依赖于传输速度,服务器程序要传送大约10亿字节的数据给客户端程序,使用单点传送,那么每个客户程序都要要复制一份数据,如果,互联网上有10000个客户端要收看这个音乐会,那么服务器程序通过internet要传送10000g的数据,这必然导致网络阻塞,降低网络的传输速度。

如果服务器程序要将同一信息发送给多个客户端,那么服务器程序和客户程序可以利用多点传送(multicasting)方式进行通信。多点传送就是服务程序对专用的多点传送组的ip地址和端口发送一系列自寻址数据包,通过加入操作ip地址被多点传送socket注册,通过这个点客户程序可以接收发送给组的自寻址包(同样客户程序也可以给这个组发送自寻址包),一旦客户程序读完所有要读的自寻址数据包,那么可以通过离开组操作离开多点传送组。

注意:ip地址224.0.0.1 到 239.255.255.255(包括)均为保留的多点传送组地址。

网络api通过multicastsocket类和multicastsocket,以及一些辅助类(比如networkinterface)支持多点传送,当一个客户程序要加入多点传送组时,就创建一个multicastsocket对象。multicastsocket(int port)构造函数允许应用程序指定端口(通过port参数)接收自寻址包,端口必须与服务程序的端口号相匹配,要加入多点传送组,客户程序调用两个joingroup()方法中的一个,同样要离开传送组,也要调用两个leavegroup()方法中的一个。

由于multicastsocket扩展了datagramsocket类,一个multicastsocket对象就有权访问datagramsocket方法。

list6是mcclient的源代码,这段代码示范了一个客户端加入多点传送组的例子。

listing 6: mcclient.java

// mcclient.java

import java.io.*;

import java.net.*;

class mcclient

{

public static void main (string [] args) throws ioexception

{

// create a multicastsocket bound to local port 10000. all

// multicast packets from the server program are received

// on that port.

multicastsocket s = new multicastsocket (10000);

// obtain an inetaddress object that contains the multicast

// group address 231.0.0.1. the inetaddress object is used by

// datagrampacket.

inetaddress group = inetaddress.getbyname ("231.0.0.1");

// join the multicast group so that datagram packets can be

// received.

s.joingroup (group);

// read several datagram packets from the server program.

for (int i = 0; i

mcclient创建了一个绑定端口号10000的multicastsocket对象,接下来他获得了一个inetaddress子类对象,该子类对象包含多点传送组的ip地址231.0.0.0,然后通过joingroup(inetaddress addr)方法加入多点传送组中,接下来mcclient接收10个自寻址包,同时输出他们的内容,然后使用leavegroup(inetaddress addr)方法离开传送组,最后关闭套接字。

也许你对使用两个字节数组buffer 和 buffer2感到奇怪,当接收到一个自寻址包后,getdata()方法返回一个引用,自寻址包的长度是256个字节,如果要输出所有数据,在输出完实际数据后会有很多空格,这显然是不合理的,所以我们必须去掉这些空格,因此我们创建一个小的字节数组buffer2,buffer2的实际长度就是数据的实际长度,通过调用datagrampacket's getlength()方法来得到这个长度。从buffer 到 buffer2快速复制getlength()的长度的方法是调用system.arraycopy()方法。

list7 mcserver的源代码显示了服务程序是怎样工作的。

listing 7: mcserver.java

// mcserver.java

import java.io.*;

import java.net.*;

class mcserver

{

public static void main (string[] args) throws ioexception

{

system.out.println ("server starting...\n");

// create a multicastsocket not bound to any port.

multicastsocket s = new multicastsocket ();

// because multicastsocket subclasses datagramsocket, it is

// legal to replace multicastsocket s = new multicastsocket ();

// with the following line.

// datagramsocket s = new datagramsocket ();

// obtain an inetaddress object that contains the multicast

// group address 231.0.0.1. the inetaddress object is used by

// datagrampacket.

inetaddress group = inetaddress.getbyname ("231.0.0.1");

// create a datagrampacket object that encapsulates a reference

// to a byte array (later) and destination address

// information. the destination address consists of the

// multicast group address (as stored in the inetaddress object)

// and port number 10000 -- the port to which multicast datagram

// packets are sent. (note: the dummy array is used to prevent a

// nullpointerexception object being thrown from the

// datagrampacket constructor.)

byte [] dummy = new byte [0];

datagrampacket dgp = new datagrampacket (dummy,

0,

group,

10000);

// send 30000 strings to the port.

for (int i = 0; i

mcserver创建了一个multicastsocket对象,由于他是datagrampacket对象的一部分,所以他没有绑定端口号,datagrampacket有多点传送组的ip地址(231.0.0.0),一旦创建datagrampacket对象,mcserver就进入一个发送30000条的文本的循环中,对文本的每一行均要创建一个字节数组,他们的引用均存储在前面创建的datagrampacket对象中,通过send()方法,自寻址包发送给所有的组成员。

在编译了mcserver 和 mcclient后,通过输入java mcserver开始运行mcserver,最后再运行一个或多个mcclient。

结论

本文通过研究套接字揭示了java的网络api的应用方法,我们介绍了套接自的慨念和套接字的组成,以及流套接字和自寻址套接字,以及如何使用inetaddress, socket, serversocket, datagrampacket, datagramsocket和multicastsocket类。在完成本文后就可以编写基本的底层通讯程序。


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值