网络编程接口socket.docx
《网络编程接口socket.docx》由会员分享,可在线阅读,更多相关《网络编程接口socket.docx(24页珍藏版)》请在冰点文库上搜索。
网络编程接口socket
使用Java语言还可以编写底层的网络通信程序,这是通过包中提供的Socket类以及ServerSocket等类实现的。
本章讨论如何通过socket使用TCP/IP或UDP协议实现在网络上两个程序间建立连接并交换数据。
Java套接字编程
机器标识
连接到Internet上的计算机使用IP地址或域名来唯一标识一台计算机,在局域网上的计算机则可以使用名称标识。
要实现网络通信,首先需要知道计算机的地址。
在包中提供了InetAddress类对象来表示计算机地址。
InetAddress类没有提供构造方法,要得到一个InetAddress类对象,需要使用该类的静态方法。
∙publicstaticInetAddressgetByName(Stringhost)返回给定主机名或点分十进制表示的主机的IP地址。
∙publicstaticInetAddress[]getAllByName(Stringhost)返回给定主机名或点分十进制表示的主机的所有IP地址数组。
∙publicstaticInetAddressgetLocalHost()返回本地主机的IP地址。
上述方法在指定的主机未知时将抛出UnknownHostException异常。
InetAddress类其他方法:
∙publicStringgetHostName()返回该IP地址的主机名字符串。
∙publicStringgetHostAddress()返回主机IP地址的字符串。
∙publicbyte[]getAddress()返回四个元素的表示IP地址的字节数组。
下面程序通过命令行给出一台主机的域名,程序将输出该机器的网络地址。
程序GetIPAddress.java
import.*;
publicclassGetIPAddress{
publicstaticvoidmain(Stringargs[])throwsException{
if(args.length!
=1){
System.out.println("Usage:
GetIPAddressDomainName");
System.exit
(1);
}
InetAddressaddress=InetAddress.getByName(args[0]);
System.out.println(address);
System.out.println("Name:
"+address.getHostName());
System.out.println("Address:
"+address.getHostAddress());
}
}
_____________________________________________________________________________▃
要运行该程序,机器必须连到网络上。
下面是程序运行的一个结果:
图GetIPAddress程序的运行结果
套接字通信
在网络上,很多应用都是采用客户/服务器(C/S)结构的。
实现网络通信必须将两台机器连接起来建立一个双向通信链路,这个双向通信链路的每一端称为一个套接字(socket)。
1.套接字的构成
我们知道,在Internet上可以使用IP地址唯一标识一台主机。
但一台主机可能提供多种服务,而仅用IP地址还不能唯一标识一个服务。
因此通常使用一个整数来标识该机器上的某个服务,这个整数就是端口号(port)。
端口号是用16位整数标识,共有65536个端口号。
端口号并不是机器上实际存在的物理位置,而是一种软件上的抽象。
端口号分为两类。
一类是由因特网名字和号码指派公司ICANN分配给一些常用的应用层程序固定使用的熟知端口(well-knownport),其数值为1~1024。
例如HTTP服务的端口号为80,FTP服务的端口号为21。
表1列出了几种常用的熟知端口号。
表1常用服务的端口号
服务
FTP
Telnet
SMTP
DNS
HTTP
SNMP
端口号
21
23
25
53
80
161
另一类端口为一般端口,用来随时分配给请求通信的客户进程。
为了在通信时不致发生混乱,就必须把端口号和主机的IP地址结合在一起使用。
一个TCP连接由它的两个端点来标识,而每一个端点又是由IP地址和端口号决定的。
TCP连接的端点称为套接字(socket),套接字是由IP地址和端口号构成的,如图2所示:
图2套接字的构成
这里,131.6.23.13为IP地址,1500为端口号,因此套接字为131.6.23.13,1500。
2.套接字通信与套接字类
一般来说,运行在一台特定机器上的某个服务器(如HTTP服务器)都有一个套接字绑定到该服务器上。
服务器只是等待、监听客户的连接请求。
在客户端,客户机需要知道运行服务器的主机名和端口号。
为了建立连接请求,客户机试图与服务器机器上的指定端口号上的服务连接,这个请求过程如图3所示:
图3客户向服务器请求连接
如果正常,服务器将接受连接请求。
一旦接受了请求,服务器将创建一个新的绑定到另一个端口号的套接字,然后使用该套接字与客户通信。
这样,服务器可以使用原来的套接字继续监听连接请求,如图4所示。
图4服务器接受客户的连接
在客户端,如果连接被接受,就会创建一个套接字(socket),客户就可以使用该套接字与服务器通信。
注意,客户端的套接字并没有绑定到与服务器连接的端口号上,相反客户被指定客户程序所在机器上的一个端口号上。
现在客户与服务器就可以读写套接字进行通信了。
为了实现套接字通信,在包中提供了两个类:
Socket和ServerSocket。
它们分别实现连接的客户端和服务器端的套接字。
Socket类的常用构造方法:
∙publicSocket(Stringhost,intport)throwsUnknownHostException,IOException创建一个流式套接字对象并将其连接到命名主机的指定端口上。
host为主机名,port为端口号。
∙publicSocket(InetAddressaddress,intport)throwsIOException创建一个流式套接字对象并将其连接到指定IP地址的指定端口上。
address为主机的IP地址,port为端口号。
∙publicSocket(Stringhost,intport,InetAddresslocalAddr,intlocalPort)throwsIOException
∙publicSocket(InetAddressaddress,intport,InetAddresslocalAddr,intlocalPort)throwsIOException
Socket类提供的主要方法有:
∙publicInetAddressgetInetAddress()返回该套接字所连接的IP地址。
∙publicintgetPort()返回该套接字所连接的远程端口号。
∙publicsynchronizedvoidclose()throwsIOException关闭套接字对象。
∙publicInputStreangetInputStream()throwsIOException获得套接字绑定的数据输入流。
∙publicOutputStreamgetOutputStream()throwsIOException获得套接字绑定的数据输出流。
ServerSocket类用在服务器端。
客户与服务器通信,客户向服务器提出请求,服务器监听请求,一旦监听到客户请求,服务器也要建立一个套接字。
这可通过ServerSocket类实现。
ServerSocket类的构造方法如下:
∙ServerSocket(intport)创建绑定到指定端口port上的服务器套接字。
∙ServerSocket(intport,intbacklog)参数backlog指定最大的队列数,即服务器所能支持的最大连接数。
∙ServerSocket(intport,intbacklog,InetAddressbindAddr)参数bindAddr为所绑定的地址。
上面的构造方法都声明了抛出IOException异常,因此创建对象时应该捕获异常或声明抛出异常。
注意,因为有些端口号已被特殊的服务占用,所以应该选择大于1024的端口号。
ServerSocket类提供的主要方法:
∙publicSocketaccept()throwsIOException该方法是ServerSocket类的重要方法,调用该方法将阻塞当前系统服务线程,直到有客户连接。
当有客户连接时,该方法返回一个Socket对象。
正是通过该Socket对象,服务器才可以与客户通信。
∙publicvoidclose()throwsIOException关闭ServerSocket对象。
3.一个简单的客户和服务器通信的实例
无论一个套接字的通信功能多么齐全、程序多么复杂,其基本结构都是一样的,都包括以下四个基本步骤:
(1)创建socket对象;
(2)打开连接到socket的输入输出流;
(3)按照一定协议对socket进行读写操作;
(4)关闭socket。
图5说明了服务器和客户端所发生的动作。
图5通信双方建立连接的过程
在服务器端,首先指定一个端口号,创建一个ServerSocket对象。
然后调用accept()方法等待客户连接。
如果客户请求一个连接,服务器使用accept()方法将返回一个Socket对象。
在客户端,用指定的服务器主机名或IP地址及端口号创建一个Socket对象,该对象试图连接到指定主机指定端口的服务。
当两端都返回Socket对象后,就可以分别在Socket对象上调用getInputStream()和getOutputStream()方法,得到输入输出流对象。
这里要注意,服务器端的输出流对应于客户端的输入流,服务器端的输入流对应于客户端的输出流。
双方建立了输入输出流后就可以进行通信了。
最后,通信结束应该调用close()方法关闭套接字,释放连接的资源。
下面是一个简单的字符界面的聊天程序。
在服务器端使用端口号4700创建服务器套接字。
服务器端程序如下:
程序ServerDemo.java
importjava.io.*;
import.*;
publicclassServerDemo{
publicstaticvoidmain(Stringargs[]){
try{
ServerSocketserver=newServerSocket(4700);
Socketsocket=server.accept();
Stringline;
BufferedReaderis=newBufferedReader(
newInputStreamReader(socket.getInputStream()));
PrintWriteros=newPrintWriter(socket.getOutputStream());
BufferedReadersin=newBufferedReader(
newInputStreamReader(System.in));
System.out.println("Client:
"+is.readLine());
System.out.print("Server:
");
line=sin.readLine();
while(!
line.equals("bye")){
os.println(line);
os.flush();
System.out.println("Client:
"+is.readLine());
System.out.print("Server:
");
line=sin.readLine();
}
sin.close();
os.close();
is.close();
socket.close();
server.close();
}catch(Exceptione){
System.out.println("Error:
"+e);
}
}
}
_____________________________________________________________________________▃
客户端端程序如下:
程序ClientDemo.java
importjava.io.*;
import.*;
publicclassClientDemo{
publicstaticvoidmain(Stringargs[]){
try{
Socketsocket=newSocket("server",4700);
BufferedReaderis=newBufferedReader(
newInputStreamReader(socket.getInputStream()));
PrintWriteros=newPrintWriter(socket.getOutputStream());
BufferedReadersin=newBufferedReader(
newInputStreamReader(System.in));
Stringreadline;
System.out.print("Client:
");
readline=sin.readLine();
while(!
readline.equals("bye")){
os.println(readline);
os.flush();
System.out.println("Server:
"+is.readLine());
System.out.print("Client:
");
readline=sin.readLine();
}
sin.close();
os.close();
is.close();
socket.close();
}catch(Exceptione){
System.out.println("Error:
"+e);
}
}
}
_____________________________________________________________________________▃
该程序首先建立一个Socket对象,其中“server”为服务器机器名,4700为服务器提供连接服务的端口号。
如果在一个机器上测试该程序,可以将“server”改为所在机器的名称或使用“localhost”,也可以使用“127.0.0.1”地址。
要测试该程序,应该首先运行服务器程序,然后运行客户程序,并且客户程序先向服务器发送消息。
图6和7分别给出了该程序的运行效果。
图6服务器端程序运行效果
图7客户端程序运行效果
4.为多个客户提供服务
在上面的例子中,服务器只能为一个客户提供服务。
在实际应用中,往往是在服务器机器上运行一个服务器,它接收来自其他多个客户的请求,为多个客户提供服务。
下面程序提供的功能是客户程序向服务器发送一个表示圆的半径的数,服务器为其计算圆的面积并将计算结果发送给客户。
这里为实现服务器为多个客户服务,使用了多线程的机制。
下面是服务器端程序:
程序MultiThreadServer.java
importjava.io.*;
import.*;
publicclassMultiThreadServer{
publicstaticvoidmain(String[]args){
try{
ServerSocketserverSocket=newServerSocket(8000);
intclientNo=1;
while(true){
SocketconnectToClient=serverSocket.accept();
System.out.println("Startthreadforclient"+clientNo);
InetAddressclientInetAddress=
connectToClient.getInetAddress();
System.out.println("Client"+clientNo+"'shostnameis"
+clientInetAddress.getHostName());
System.out.println("Client"+clientNo+"'sIPaddressis"
+clientInetAddress.getHostAddress());
HandleClientthread=newHandleClient(connectToClient);
thread.start();
clientNo++;
}
}catch(IOExceptionex){
System.err.println(ex);
}
}
}
classHandleClientextendsThread{
privateSocketconnectToClient;
publicHandleClient(Socketsocket){
connectToClient=socket;
}
publicvoidrun(){
try{
DataInputStreamisFromClient=newDataInputStream(
connectToClient.getInputStream());
DataOutputStreamosToClient=newDataOutputStream(
connectToClient.getOutputStream());
while(true){
doubleradius=isFromClient.readDouble();
System.out.println("radiusreceivedfromclient:
"+radius);
doublearea=radius*radius*Math.PI;
osToClient.writeDouble(area);
osToClient.flush();
System.out.println("Areais:
"+area);
}
}catch(IOExceptionex){
System.err.println(ex);
}
}
}
_____________________________________________________________________________▃
下面是客户端程序:
程序Client.java
importjava.io.*;
import.*;
importjava.util.*;
publicclassClient{
publicstaticvoidmain(String[]args){
try{
SocketconnectToServer=newSocket("localhost",8000);
DataInputStreamisFromServer=newDataInputStream(
connectToServer.getInputStream());
DataOutputStreamosToServer=newDataOutputStream(
connectToServer.getOutputStream());
while(true){
System.out.print("Pleaseinputaradius:
");
Scannersc=newScanner(System.in);
doubleradius=sc.nextDouble();
osToServer.writeDouble(radius);
osToServer.flush();
doublearea=isFromServer.readDouble();
System.out.println("Areareceivedfromserver:
"+area);
}
}catch(IOExceptionex){
System.err.println(ex);
}
}
}
_____________________________________________________________________________▃
该程序的运行结果如图6和7所示:
图6服务器端程序运行效果
图7客户端程序运行效果
数据包通信
上一节讲的socket通信是一种流式通信,本节讨论通过socket实现数据包通信。
1流式通信和数据包通信
当编写网络程序时,有两种通信可供选择:
流式通信和数据包通信。
流式通信使用TCP(TransferControlProtocol)协议,该协议是面向连接的协议。
使用这种协议要求发送方和接收方都要建立socket连接,一旦两个socket建立起来,它们就可以进行双向通信,双方都可以发送和接收数据。
数据包通信使用用户数据包协议UDP(UserDatagramProtocol),该协议是一种无连接的协议。
使用这种协议通信,每个数据包都是一个独立的信息单元,它包括完整的目的地址,数据包在网络上以任何可能的路径传往目的地,因此数据能否到达目的地、到达的时间以及内容的正确性都是不能保证的,该协议提供的是不可靠的服务。
在网络的传输层既然提供了两种协议,那么在实际的应用中到底应该使用哪种协议?
这要取决于不同的应用情况,下面是两种协议的比较:
①使用UDP时,每个数据包都给出了完整的地址信息,因此无需建立发送方和接收方的连接。
对于TCP,由于它是一个面向连接的协议,在通信之前必须建立双方的连接,因此在TCP中多了一个建立连接的时间。
②使用UDP传输数据时是有大小限制的,每个数据包必须不大于64KB。
而使用TCP就没有这方面的限制,一旦连接建立起来,就可以传输大量的数据。
③UDP是不可靠的协议,发送方发送的数据不一定以相同的次序到达接收方。
而TCP是可靠的协议,它确保接收方完全正确地获取发送方所发送的数据。
④TCP使用较广泛,如TELNET远程登录、FTP文件传输都需要不定长度的数据可靠