用Java开发网络软件非常方便和强大
Java的这种力量来源于他独有的一套强大的用于网络的 API
这些API是一系列的类和接口
均位于包和中
在这篇文章中我们将介绍套接字(Socket)慨念
同时以实例说明如何使用Network API操纵套接字
在完成本文后
你将具备编写网络低端通讯软件的能力
什么是套接字(Socket)?
Network API是典型的用于基于TCP/IP网络Java程序与其他程序通讯Network API依靠Socket进行通讯Socket可以看成在两个程序进行通讯连接中的一个端点一个程序将一段信息写入Socket中该Socket将这段信息发送给另外一个Socket中使这段信息能传送到其他程序中如图
我们来分析一下图Host A上的程序A将一段信息写入Socket中Socket的内容被Host A的网络管理软件访问并将这段信息通过Host A的网络接口卡发送到Host BHost B的网络接口卡接收到这段信息后传送给Host B的网络管理软件网络管理软件将这段信息保存在Host B的Socket中然后程序B才能在Socket中阅读这段信息
假设在图的网络中添加第三个主机Host C那么Host A怎么知道信息被正确传送到Host B而不是被传送到Host C中了呢?基于TCP/IP网络中的每一个主机均被赋予了一个唯一的IP地址IP地址是一个位的无符号整数由于没有转变成二进制因此通常以小数点分隔如正如所见IP地址均由四个部分组成每个部分的范围都是以表示位地址
值得注意的是IP地址都是位地址这是IP协议版本(简称Ipv)规定的目前由于IPv地址已近耗尽所以IPv地址正逐渐代替Ipv地址Ipv地址则是位无符号整数
假设第二个程序被加入图的网络的Host B中那么由Host A传来的信息如何能被正确的传给程序B而不是传给新加入的程序呢?这是因为每一个基于TCP/IP网络通讯的程序都被赋予了唯一的端口和端口号端口是一个信息缓沖区用于保留Socket中的输入/输出信息端口号是一个位无符号整数范围是以区别主机上的每一个程序(端口号就像房屋中的房间号)低于的短口号保留给标准应用程序比如pop的端口号就是每一个套接字都组合进了IP地址端口端口号这样形成的整体就可以区别每一个套接字t下面我们就来谈谈两种套接字流套接字和自寻址数据套接字
流套接字(Stream Socket)
无论何时在两个网络应用程序之间发送和接收信息时都需要建立一个可靠的连接流套接字依靠TCP协议来保证信息正确到达目的地实际上IP包有可能在网络中丢失或者在传送过程中发生错误任何一种情况发生作为接受方的 TCP将联系发送方TCP重新发送这个IP包这就是所谓的在两个流套接字之间建立可靠的连接
流套接字在C/S程序中扮演一个必需的角色客户机程序(需要访问某些服务的网络应用程序)创建一个扮演服务器程序的主机的IP地址和服务器程序(为客户端应用程序提供服务的网络应用程序)的端口号的流套接字对象
客户端流套接字的初始化代码将IP地址和端口号传递给客户端主机的网络管理软件管理软件将IP地址和端口号通过NIC传递给服务器端主机服务器端主机读到经过NIC传递来的数据然后查看服务器程序是否处于监听状态这种监听依然是通过套接字和端口来进行的如果服务器程序处于监听状态那么服务器端网络管理软件就向客户机网络管理软件发出一个积极的响应信号接收到响应信号后客户端流套接字初始化代码就给客户程序建立一个端口号并将这个端口号传递给服务器程序的套接字(服务器程序将使用这个端口号识别传来的信息是否是属于客户程序)同时完成流套接字的初始化
如果服务器程序没有处于监听状态那么服务器端网络管理软件将给客户端传递一个消极信号收到这个消极信号后客户程序的流套接字初始化代码将抛出一个异常对象并且不建立通讯连接也不创建流套接字对象这种情形就像打电话一样当有人的时候通讯建立否则电话将被挂起
这部分的工作包括了相关联的三个类InetAddress Socket 和 ServerSocket InetAddress对象描绘了位或位IP地址Socket对象代表了客户程序流套接字ServerSocket代表了服务程序流套接字所有这三个类均位于包中
InetAddress类
InetAddress类在网络API套接字编程中扮演了一个重要角色参数传递给流套接字类和自寻址套接字类构造器或非构造器方法InetAddress描述了位或位IP地址要完成这个功能InetAddress类主要依靠两个支持类InetAddress 和 InetAddress这三个类是继承关系InetAddrress是父类InetAddress 和 InetAddress是子类
由于InetAddress类只有一个构造函数而且不能传递参数所以不能直接创建InetAddress对象比如下面的做法就是错误的
InetAddress ia = new InetAddress ();
但我们可以通过下面的个工厂方法创建来创建一个InetAddress对象或InetAddress数组
getAllByName(String host)方法返回一个InetAddress对象的引用每个对象包含一个表示相应主机名的单独的IP地址这个IP地址是通过host参数传递的对于指定的主机如果没有IP地址存在那么这个方法将抛出一个UnknownHostException 异常对象
getByAddress(byte [] addr)方法返回一个InetAddress对象的引用这个对象包含了一个Ipv地址或Ipv地址Ipv地址是一个字节数组Ipv地址是一个字节地址数组如果返回的数组既不是字节的也不是字节的那么方法将会抛出一个UnknownHostException异常对象
getByAddress(String host byte [] addr)方法返回一个InetAddress对象的引用这个InetAddress对象包含了一个由host和字节的addr数组指定的IP地址或者是host和字节的addr数组指定的IP地址如果这个数组既不是字节的也不是位字节的那么该方法将抛出一个UnknownHostException异常对象
getByName(String host)方法返回一个InetAddress对象该对象包含了一个与host参数指定的主机相对应的IP地址对于指定的主机如果没有IP地址存在那么方法将抛出一个UnknownHostException异常对象
getLocalHost()方法返回一个InetAddress对象这个对象包含了本地机的IP地址考虑到本地主机既是客户程序主机又是服务器程序主机为避免混乱我们将客户程序主机称为客户主机将服务器程序主机称为服务器主机
上面讲到的方法均提到返回一个或多个InetAddress对象的引用实际上每一个方法都要返回一个或多个InetAddress/InetAddress对象的引用调用者不需要知道引用的子类型相反调用者可以使用返回的引用调用InetAddress对象的非静态方法包括子类型的多态以确保重载方法被调用
InetAddress和它的子类型对象处理主机名到主机IPv或IPv地址的转换要完成这个转换需要使用域名系统下面的代码示范了如何通过调用getByName(String host)方法获得InetAddress子类对象的方法这个对象包含了与host参数相对应的IP地址
InetAddress ia = InetAddressgetByName ());
一但获得了InetAddress子类对象的引用就可以调用InetAddress的各种方法来获得InetAddress子类对象中的IP地址信息比如可以通过调用getCanonicalHostName()从域名服务中获得标准的主机名getHostAddress()获得IP地址getHostName()获得主机名isLoopbackAddress()判断IP地址是否是一个loopback地址
List 是一段示范代码InetAddressDemo
// InetAddressDemojava
import *;
class InetAddressDemo
{
public static void main (String [] args) throws UnknownHostException
{
String host = localhost;
if (argslength == )
host = args [];
InetAddress ia = InetAddressgetByName (host);
Systemoutprintln (Canonical Host Name = +
iagetCanonicalHostName ());
Systemoutprintln (Host Address = +
iagetHostAddress ());
Systemoutprintln (Host Name = +
iagetHostName ());
Systemoutprintln (Is Loopback Address = +
iaisLoopbackAddress ());
}
}
当无命令行参数时代码输出类似下面的结果
Canonical Host Name = localhost
Host Address =
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()方法关闭流和流套接字下面的代码创建了一个服务程序主机地址为端口号为的Socket对象然后从这个新创建的Socket对象中读取输入流然后再关闭流和Socket对象
Socket s = new Socket ( );
InputStream is = sgetInputStream ();
// Read from the stream
isclose ();
sclose ();
接下面我们将示范一个流套接字的客户程序这个程序将创建一个Socket对象Socket将访问运行在指定主机端口上的服务程序如果访问成功客户程序将给服务程序发送一系列命令并打印服务程序的响应List使我们创建的程序SSClient的源代码
Listing : SSClientjava
// SSClientjava
import javaio*;
import *;
class SSClient
{
public static void main (String [] args)
{
String host = localhost;
// If user specifies a commandline argument that argument
// represents the host name
if (argslength == )
host = args [];
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
s = new Socket (host );
// Create an input stream reader that chains to the sockets
// byteoriented input stream The input stream reader
// converts bytes read from the socket to characters The
// conversion is based on the platforms default character
// set
InputStreamReader isr;
isr = new InputStreamReader (sgetInputStream ());
// 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 sockets 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 platforms default character set
pw = new PrintWriter (sgetOutputStream () true);
// Send the DATE command to the server
pwprintln (DATE);
// Obtain and print the current date/time
Systemoutprintln (brreadLine ());
// Send the PAUSE command to the server This allows several
// clients to start and verifies that the server is spawning
// multiple threads
pwprintln (PAUSE);
// Send the DOW command to the server
pwprintln (DOW);
// Obtain and print the current day of week
Systemoutprintln (brreadLine ());
// Send the DOM command to the server
pwprintln (DOM);
// Obtain and print the current day of month
Systemoutprintln (brreadLine ());
// Send the DOY command to the server
pwprintln (DOY);
// Obtain and print the current day of year
Systemoutprintln (brreadLine ());
}
catch (IOException e)
{
Systemoutprintln (etoString ());
}
finally
{
try
{
if (br != null)
brclose ();
if (pw != null)
pwclose ();
if (s != null)
sclose ();
}
catch (IOException e)
{
}
}
}
}
运行这段程序将会得到下面的结果
Tue Jan :: CST
TUESDAY
SSClient创建了一个Socket对象与运行在主机端口的服务程序联系主机的IP地址由host变量确定SSClient将获得Socket的输入输出流围绕BufferedReader的输入流和PrintWriter的输出流对字符串进行读写操作就变得非常容易SSClient个服务程序发出各种date/time命令并得到响应每个响应均被打印一旦最后一个响应被打印将执行Try/Catch/Finally结构的Finally子串Finally子串将在关闭Socket之前关闭BufferedReader 和 PrintWriter
在SSClient源代码编译完成后可以输入java SSClient 来执行这段程序如果有合适的程序运行在不同的主机上采用主机名/IP地址为参数的输入方式比如是运行服务器程序的主机那么输入方式就是java SSClient
技巧
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对象来监听端口的连接请求如果成功服务程序将等待连接输入开始一个线程处理连接并响应来自客户程序的命令下面就是这段程序的代码
Listing : SSServerjava
// SSServerjava
import javaio*;
import *;
import javautil*;
class SSServer
{
public static void main (String [] args) throws IOException
{
Systemoutprintln (Server starting\n);
// Create a server socket that listens for incoming connection
// requests on port
ServerSocket server = new ServerSocket ();
while (true)
{
// Listen for incoming connection requests from client
// programs establish a connection and return a Socket
// object that represents this connection
Socket s = serveraccept ();
Systemoutprintln (Accepting Connection\n);
// Start a thread to handle the connection
new ServerThread (s)start ();
}
}
}
class ServerThread extends Thread
{
private Socket s;
ServerThread (Socket s)
{
thiss = s;
}
public void run ()
{
BufferedReader br = null;
PrintWriter pw = null;
try
{
// Create an input stream reader that chains to the sockets
// byteoriented input stream The input stream reader
// converts bytes read from the socket to characters The
// conversion is based on the platforms default character
// set
InputStreamReader isr;
isr = new InputStreamReader (sgetInputStream ());
// 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 sockets 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 platforms default character set
pw = new PrintWriter (sgetOutputStream () true);
// Create a calendar that makes it possible to obtain date
// and time information
Calendar c = CalendargetInstance ();
// 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 programs next command
String cmd = brreadLine ();
// Exit if client program has closed its output stream
if (cmd == null)
break;
// Convert command to uppercase for ease of comparison
cmd = cmdtoUpperCase ();
// If client program sends BYE command terminate
if (cmdstartsWith (BYE))
break;
// If client program sends DATE or TIME command return
// current date/time to the client program
if (cmdstartsWith (DATE) || cmdstartsWith (TIME))
pwprintln (cgetTime ()toString ());
// If client program sends DOM (Day Of Month) command
// return current day of month to the client program
if (cmdstartsWith (DOM))
pwprintln ( + cget (CalendarDAY_OF_MONTH));
// If client program sends DOW (Day Of Week) command
// return current weekday (as a string) to the client
// program
if (cmdstartsWith (DOW))
switch (cget (CalendarDAY_OF_WEEK))
{
case CalendarSUNDAY : pwprintln (SUNDAY);
break;
case CalendarMONDAY : pwprintln (MONDAY);
break;
case CalendarTUESDAY : pwprintln (TUESDAY);
break;
case CalendarWEDNESDAY: pwprintln (WEDNESDAY);
break;
case CalendarTHURSDAY : pwprintln (THURSDAY);
break;
case CalendarFRIDAY : pwprintln (FRIDAY);
break;
case CalendarSATURDAY : pwprintln (SATURDAY);
}
// If client program sends DOY (Day of Year) command
// return current day of year to the client program
if (cmdstartsWith (DOY))
pwprintln ( + cget (CalendarDAY_OF_YEAR));
// If client program sends PAUSE command sleep for three
// seconds
if (cmdstartsWith (PAUSE))
try
{
Threadsleep ();
}
catch (InterruptedException e)
{
}
}
while (true);
{
catch (IOException e)
{
Systemoutprintln (etoString ());
}
finally
{
Systemoutprintln (Closing Connection\n);
try
{
if (br != null)
brclose ();
if (pw != null)
pwclose ();
if (s != null)
sclose ();
}
catch (IOException e)
{
}
}
}
}
运行这段程序将得到下面的输出
Server starting
Accepting Connection
Closing Connection
SSServer的源代码声明了一对类SSServer 和ServerThreadSSServer的main()方法创建了一个ServerSocket对象来监听端口上的连接请求如果成功 SSServer进入一个无限循环中交替调用ServerSocket的 accept() 方法来等待连接请求同时启动后台线程处理连接(accept()返回的请求)线程由ServerThread继承的start()方法开始并执行ServerThread的run()方法中的代码
一旦run()方法运行线程将创建BufferedReader PrintWriter和 Calendar对象并进入一个循环这个循环由读(通过BufferedReader的 readLine())来自客户程序的一行文本开始文本(命令)存储在cmd引用的string对象中如果客户程序过早的关闭输出流会发生什么呢?答案是cmd将得不到赋值
注意必须考虑到这种情况在服务程序正在读输入流时客户程序关闭了输出流如果没有对这种情况进行处理那么程序将产生异常
一旦编译了SSServer的源代码通过输入Java SSServer来运行程序在开始运行SSServer后就可以运行一个或多个SSClient程序