服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

java网络编程之传输控制协议(三)

  五、建立tcp客户端

  讨论了套接字类的功能后,我们将分析一个完整的tcp客户端程序。此处我们将看到的客户端程序是一个daytime客户端,它连接到一个daytime服务器程序以读取当前的日期和时间。建立套接字连接并读取信息是一个相当简单的过程,只需要少量的代码。默认情况下daytime服务运行在13端口上。并非每台计算机都运行了daytime服务器程序,但是unix服务器是客户端运行的很好的系统。如果你没有访问unix服务器的权限,在第七部分我们给出了tcp daytime服务器程序代码--有了这段代码客户端就可以运行了。

  daytimeclient的代码

import java.net.*
import java.io.*;
public class daytimeclient
{
public static final int service_port = 13;

public static void main(string args[])
{
// 检查主机名称参数
if (args.length != 1)
{
system.out.println ("syntax - daytimeclient host");
return;
}

// 获取服务器程序的主机名称
string hostname = args[0];

try
{
// 获取一个连接到daytime服务的套接字
socket daytime = new socket (hostname,
service_port);

system.out.println ("connection established");

// 在服务器程序停止的情况下设置套接字选项
daytime.setsotimeout ( 2000 );

// 从服务器程序读取信息
bufferedreader reader = new bufferedreader (
new inputstreamreader
(daytime.getinputstream()
));

system.out.println ("results : " +
reader.readline());

// 关闭连接
daytime.close();
}
catch (ioexception ioe)
{
system.err.println ("error " + ioe);
}
}
}


  daytimeclient是如何工作的

  daytime应用程序是很容易理解的,它使用了文章前面谈到的概念。建立套接字、获取输入流,在很少的事件中(在连接时像daytime一样简单的服务器程序失败)激活超时设置。不是连接已筛选过的流,而是把有缓冲的读取程序连接到套接字输入流,并且把结果显示给用户。最后,在关闭套接字连接后客户端终止。这是你可能得到的最简单的套接字应用程序了--复杂性来自实现的网络协议,而不是来自具体网络的编程。

  运行daytimeclient

  运行上面的应用程序很简单。简单地把运行daytime服务的计算机的主机名称作为命令行参数指定并运行它就可以了。如果daytime服务器程序使用了非标准的端口号(在后面会讨论),记得需要改变端口号并重新编译。

  例如,如果服务器程序在本机上,为了运行客户端将使用下面的命令:

java daytimeclient localhost

  注意

  daytime服务器程序必须正在运行中,否则该客户端程序将不能建立连接。例如如果你正在使用wintel系统而不是unix,那么你需要运行daytimeserver(后面会谈到


  六、serversocket类

  服务器套接字是一种特定类型的套接字,它用于提供tcp服务。客户端套接字绑定到本地计算机的任何空的端口,并且连接到特定服务器程序的端口和主机。服务器套接字与它的差别是它们绑定到本地计算机的某个特定的端口,这样远程客户端才能定位某种服务。客户端套接字连接只能连接到一台计算机,然而服务器套接字能够满足多个客户端的请求。

  它工作的方法很简单--客户端知道服务运行在某个特定的端口(通常端口号是知名的,并且特定的协议使用特定的端口号,但是服务器程序也可能运行在非标准的端口上)。它们建立连接,在服务器程序内部,连接会被接受。服务器程序可以同时接受多个连接,在某个给定的时刻也可以选择只接受一个连接。某个连接被接受后,它就表现为正常的套接字,形式为socket对象--一旦你掌握了socket类,编写服务器程序就和编写客户端程序几乎一样简单了。服务器程序和客户端程序的唯一区别是服务器程序帮定到特定的端口,使用serversocket对象。serversocket对象就像创建客户端连接的工厂--你不必亲自建立socket类的实例。这些连接都模拟正常的套接字,因此你能够把输入和输出过滤流关联到这些连接上。

  1、建立serversocket

  你在建立服务器套接字后,就应该把它绑定到某个本地端口并准备接受输入的连接。当客户端试图连接的时候,它们被放入一个队列中。一旦这个队列中的所有空间都被耗尽,其它的连接的就会被拒绝。

  构造函数

  建立服务器套接字的最简单的途径是绑定到某个本地地址,该地址作为使用构造函数的唯一的参数。例如,为了在端口80(通常用于web服务器程序)上提供某个服务,将使用下面的代码片断:


try
{
// 绑定到80端口,提供tcp服务(类似与http)
serversocket myserver = new serversocket ( 80 );

// ......
}
catch (ioexception ioe)
{
system.err.println ("i/o error - " + ioe);
}


  这是serversocket构造函数的最简单的形式,但是下面有一些其它的允许更多自定义的构造函数。所有这些函数都是公共的。

  ? serversocket(int port)产生java.io.ioexception、java.lang.securityexception异常--把服务器套接字绑定到特定的端口号,这样远程客户端才能定位tcp服务。如果传递进来的值为零(zero),就使用任何空闲的端口--但是客户端可能没办法访问该服务,除非用什么方式通知了客户端端口号是多少。在默认情况下,队列的大小设置为50,但是也提供了备用的构造函数,它允许修改这个设置。如果端口已经被绑定了,或者安全性约束条件(例如安全性规则或知名端口上的操作系统约束条件)阻挡了访问,就会产生异常。

  ? serversocket(int port, int numberofclients)产生java.io.ioexception、java.lang.securityexception异常--把服务器套接字绑定到特定的端口号并为队列分配足够的空间用于支持特定数量的客户端套接字。它是serversocket(int port)构造函数的重载版本,如果端口已经被绑定了或安全性约束条件阻挡了访问,就产生异常。

  ? serversocket(int port, int numberofclients, inetaddress address)产生java.io.ioexception、java.lang.securityexception异常--把服务器套接字绑定到特定的端口号,为队列分配足够的空间以支持特定数量的客户端套接字。它是serversocket(int port, int numberofclients)构造函数的重载版本,在多地址计算机上,它允许服务器套接字绑定到某个特定的ip地址。例如,某台计算机可能有两块网卡,或者使用虚拟ip地址把它配置成像几台计算机一样工作的时候。如果地址的值为空(null),服务器套接字将在所有的本地地址上接受请求。如果端口已经被绑定了或者安全性约束条件阻挡了访问,就产生异常。

  2、使用serversocket

  虽然socket类几乎是通用的,并且有很多方法,但是server socket类没有太多的方法,除了接受请求并作为模拟客户端和服务器之间连接的socket对象的产生组件就没有几个了。其中最重要的方法是accept()方法,它接受客户端连接请求,但是还有其它几个开发者可能感到有用的方法。




  方法

  如果没有注明的话该方法就是公共的。

  ? socket accept()产生java.io.ioexception、java.lang.security异常--等待客户端向某个服务器套接字请求连接,并接受连接。它是一种阻塞(blocking)i/o操作,并且不会返回,直到建立一个连接(除非设置了超时套接字选项)。当连接建立时,它将作为socket对象被返回。当接受连接的时候,每个客户端请求都被默认的安全管理程序验证,这使得接受一定ip地址并阻塞其它ip地址、产生异常成为可能。但是,服务器程序不必依赖安全管理程序阻塞或终止连接--可以通过调用客户端套接字的getinetaddress()方法确定客户端的身份。

  ? void close()产生java.io.ioexception异常--关闭服务器套接字,取消tcp端口的绑定,允许其它的服务使用该端口。

  ? inetaddress getinetaddress()--返回服务器套接字的地址,在多地址计算机中(例如某个计算机的本地主机可以通过两个或多个ip地址访问)它可能与本地地址不同。

  ? int getlocalport()--返回服务器套接字绑定到的端口号。

  ? int getsotimeout()产生java.io.ioexception异常--返回超时套接字选项的值,该值决定accept()操作可以阻塞多少毫秒。如果返回的值为零,accept()操作无限期阻塞。

  ? void implaccept(socket socket)产生java.io.ioexception异常--这个方法允许serversocket子类传递一个未连接的套接字子类,让这个套接字对象接受输入的请求。使用implaccept方法接受连接时,重载的serversocket.accept()方法可以返回已连接的套接字。很少开发者希望对serversocket再细分类,在不必要的情况下应该避免使用它。

  ? static void setsocketfactory ( socketimplfactory factory )产生java.io.ioexception、java.net.socketexception、java.lang.securityexception异常--为jvm指定服务器套接字产生组件。它是一个静态的方法,在jvm的生存周期中只能调用一次。如果禁止指定新的套接字产生组件,或者已经指定了一个,就会产生异常。

  ? void setsotimeout(int timeout)产生java.net.socketexception异常--为accept()操作指定一个超时值(以毫秒计算)。如果指定的值是零,超时设置就被禁止了,该操作将无限制阻塞。但是,如果允许超时设置,在accept()方法被调用的时候就启动一个计时器。当计时器期满时,产生java.io.interruptedioexception异常,并允许服务器程序执行进一步的操作。

  3、从客户端接受和处理请求

  服务器套接字的最重要的功能是接受客户端套接字。一旦获取了某个客户端套接字,服务器就可以执行服务器程序的所有"真实的工作",包括从套接字读取信息、向套接字写入信息以实现某种网络协议。发送或接收的准确数据依赖于该协议的详细情况。例如,对存储的消息提供访问的邮件服务器将监听命令并发回消息内容。telnet服务器监听键盘输入并把这些信息传递给一个登陆外壳(shell),并把输出发回网络客户端。具体协议的操作与网络的相关性很小,更多的面向编程。

  下面的代码片断演示了如果接受客户端套接字,以及i/o流怎样连接到客户端:


// 执行阻塞的读取操作,读取下一个套接字
socket nextsocket = someserversocket.accept();

// 连接到流的过滤器读取和写入程序
bufferedreader reader = new bufferedreader (new
inputstreamreader
(nextsocket.getinputstream() ) );
printwriter writer = new printwriter( new
outputstreamwriter
(nextsocket.getoutputstream() ) );


  从这个时候开始,服务器程序就可以处理任何需要完成的事务并响应客户端请求了,或者可以选择事务给另一个线程中的代码运行。请记住与java中的其它形式的i/o操作类似,从客户端读取回应的时候代码会无限制阻塞--因此为了为多个客户端并行服务,必须使用多线程。但是在简单的情形中,多个执行线程可能是不必要的,特别是在对请求响应迅速并且处理时间很短的情况下。

  建立完整实现通用internet协议的客户端/服务器应用程序需要作大量的工作,对于网络编程的新手来说这一点更为明显。它也需要其它一些技巧,例如多线程编程。从现在开始,我们聚焦于一个简单的、作为单线程应用程序执行的tcp服务器程序框架。






  七、建立tcp服务器程序

  网络编程的最有趣的部分之一是编写网络服务器。客户端发送请求并响应发回来的数据,但是服务器执行大多数真正的工作。下面的例子是一个daytime(日期时间)服务器(你可以使用上面描述的客户端测试它)。

  daytimeserver的代码


import java.net.*;
import java.io.*;

public class daytimeserver
{
public static final int service_port = 13;

public static void main(string args[])
{
try
{
// 绑定到服务端口,给客户端授予访问tcp daytime服务的权限
serversocket server = new serversocket
(service_port);

system.out.println ("daytime service started");

// 无限循环,接受客户端
for (;;)
{
// 获取下一个tcp客户端
socket nextclient = server.accept();

// 显示连接细节
system.out.println ("received request from " +
nextclient.getinetaddress() + ":" +
nextclient.getport() );

// 不读取数据,只是向消息写信息
outputstream out =
nextclient.getoutputstream();
printstream pout = new printstream (out);

// 把当前数据显示给用户
pout.print( new java.util.date() );

// 清除未发送的字节
out.flush();

// 关闭流
out.close();

// 关闭连接
nextclient.close();
}
}
catch (bindexception be)
{
system.err.println ("service already running on port " + service_port );
}
catch (ioexception ioe)
{
system.err.println ("i/o error - " + ioe);
}
}
}


  daytimeserver是如何工作的

  这是最简单的服务器程序了。这个服务器程序的第一步是建立一个serversocket。如果端口已经绑定了,将会产生一个bindexception异常,因为两个服务器程序不可能共享相同的端口。否则,就建立了服务器套接字。下一步是等待连接。

  因为daytime是个非常简单的协议,并且我们的第一个tcp服务器程序示例必须很简单,所以我们此处使用了单线程服务器程序。在简单的tcp服务器程序中通常使用无限运行的for循环,或者使用表达式的值一直为true的while循环。在这个循环中,第一行是server.accept()方法,它会阻塞代码运行直到某个客户端试图连接为止。这个方法返回一个表示某个客户端的连接的套接字。为了记录数据,该连接的ip地址和端口号被发送到system.out。你将看到每次某个人登陆进来并获取某天的时间。

  daytime是一个仅作应答(response-only)的协议,因此我们不需要担心对任何输入信息的读取过程。我们获得了一个outputstream(输出流),接着把它包装进printstream(打印流),使它工作更简单。我们在使用java.util.date类决定日期和时间后,基于tcp流把它发送给客户端。最后,我们清除了打印流中的所有数据并通过在套接字上调用close()关闭该连接。

  运行daytimeserver

  运行该服务器程序是很简单的。该服务器程序没有命令行参数。如果这个服务器程序示例需要运行在unix上,你需要把变量service_port的值该为1024,除非你关闭默认的daytime进程并作为root运行这个示例。在windows或其它操作系统上,就没有这个问题。如果需要在本机上运行该服务器程序,需要使用下面的命令:

java daytimeserver





  八、异常处理:特定套接字的异常

  网络作为通讯的媒介充满了各种问题。随着大量的计算机连接到了全球internet,遭遇到某个主机名称无法解析、某个主机从网络断开了、或者某个主机在连接的过程中被锁定了的情形在软件应用程序的生存周期中是很可能遇到的。因此,知道引起应用程序中出现的这类问题的条件并很好的处理这些问题是很重要的。当然,并不是每个程序都需要精确的控制,在简单的应用程序中你可能希望使用通用的处理方法处理各种问题。但是对于更高级的应用程序,了解运行时可能出现的特定套接字异常是很重要的。

  注意

  所有的特定套接字异常都扩展自socketexception,因此通过捕捉该异常,你可以捕捉到所有的特定套接字的异常并编写一个通用的处理程序。此外,socketexception扩展自java.io.ioexception,如果你希望提供捕捉所有i/o异常的处理程序可以使用它。

  1、 socketexception

  java.net.socketexception表现了一种通用的套接字错误,它可以表现一定范围的特定错误条件。对于更细致的控制,应用程序应该捕捉下面讨论的子类。

  2、 bindexception

  java.net.bindexception表明没有能力把套接字帮定到某个本地端口。最普通的原因是本地端口已经被使用了。

  3、connectexception

  当某个套接字不能连接到特定的远程主机和端口的时候,java.net.connectexception就会发生。发生这种情况有一个原因,例如远程服务器没有帮定到某个端口的服务,或者它被排队的查询淹没了,不能接收更多的请求。

  4、 noroutetohostexception

  当由于出现网络错误,不能找到远程主机的路由的时候产生java.net.noroutetohostexception异常。它的起因可能是本地的(例如软件应用程序运行的网络正在运行),可能是临时的网关或路由器问题,或者是套接字试图连接的远程网络的故障。另一个普通原因是防火墙和路由器阻止了客户端软件,这通常是个持久的限制。

  5、interruptedioexception

  当某个读取操作被阻塞了一段时间引起网络超时的时候产生java.net.interruptedioexception异常。处理超时问题是使代码更加牢固和可靠的很好的途径。

  九、总结

  在tcp中使用套接字通讯是你应该掌握的一种重要的技术,因为目前使用的大多数有趣的应用程序协议都是在tcp上出现的。java套接字api提供了一种清晰的、易于使用的机制,利用这种机制开发者可以作为服务器接受通讯或作为客户端启动通讯。通过使用前面讨论的概念(包括java下的输入和输出流),过渡到基于套接字的通讯是很直接的。有了建立在java.net程序包中的异常处理水平后,很容易处理运行时发生的网络错误。

扫描关注微信公众号