1.TCP/IP协议
TCP/IP协议是整个网络通信的核心协议。其中TCP协议运行在客户终端上,是集成在操作系统内的一套协议软件,它的任务是在网络上的两个机器之间实现端到端的、可靠的数据传输功能。IP协议运行在组成网络的核心设备路由器上,它也是集成在系统内的一层协议软件,负责将数据分组从源端发送到目的端,通过对整个网络拓扑结构的理解为分组的发送选择路由。值得注意的是,TCP协议运行在客户的主机中,是操作系统的一个组件,一般操作系统会默认安装该协议软件;而IP协议即运行在客户主机中,也运行在网络设备中。
(1)IP协议和IP地址
计算机网络中的每台运行了IP协议的主机,都具有一个IP地址,该地址标识网络中的一台主机。IP地址采用点分十进制方法表示。IP地址是一个32位的二进制序列,点分的每个部分占一个字节,使用十进制表达。显然每个部分最大不超过255,因为二进制的8个1(11111111)用十进制表达就是255。
IP地址由网络部分和主机部分构成,网络部分表示一个通信子网,子网内的主机可以不通过路由器而直接通信,如一个公司办公室的局域网;主机部分标识该通信子网内的主机。
为了区分IP地址的网络部分和主机部分,给出掩码的概念,掩码也用点分十进制表达,如255.255.255.0,不过掩码的前面部分是二进制1,后面部分都是二级制的0,这一点与IP地址稍有不同,并且还可以用IP地址后加一个“/”跟上掩码的全部1的数量表达掩码,如掩码255.255.255.0也可以表达为“/24”。通过主机的IP地址和网络掩码就可以计算该主机所在的网络,如主机的IP地址为192.168.2.155/24,则网络地址的计算方式是吧网络掩码同IP地址进行与运算。则上述主机的网络号为192.168.2.0。网络号标识一个网络,而主机号标识一个主机。每个网络内的主机通过局域网交换机通信,而不同网络之间的主机必须通过路由器进行通信。
《1》URL
URL称为统一资源定位符,用于标识网络上的某种资源,如一个网页链接,一个视频文件等。当用户浏览网页时,单击某一个链接,在浏览器的地址栏中就会出现该链接的网页地址,这个地址其实就是URL,用来定位要访问的网页,如:http://www.havathinker.org/bbs/index.jsp。其中“http”表示一种应用层的传输协议超文本传输协议,"www.havathinker.org"是域名,而bbs是网页所在的路径,“index.jsp"是要访问的网页。应用层协议不止是HTTP协议,还有FTP协议和File协议等。
《2》网络域名
由于IP地址是点分十进制表达,所以不容易记忆,于是发明了这种采用易于记忆的符号代替IP地址的方法。在整个Internet中,域名与IP地址一一对应,一个IP地址有唯一的域名。
域名有一定的含义,且具有一定的指导意义。如:“www.baidu.com”。顶级域名有两种,一种是通用域名,另一种是国家域名。通用域名有com(商业)、edu(教育)、gov(政府组织)、mil(军事部门)、org(非盈利组织)。国家域名如cn(中国)、us(美国)等。
在网络中传输的分组都是基于IP地址进行路由和交换的,域名无法实现这个功能,域名只是为了记忆方便而采用的一种形式,所以必须把域名重新翻译为IP地址,如访问百度网站www.baidu.com,其实后台会发生一系列IP地址的搜索过程,这个过程由DNS系统实现。DNS的主要用途就是将主机名和主机的IP地址进行映射,将名字映射到IP地址。DNS是一个分布式数据库服务器系统,存储域名和对应IP的信息。当用户使用域名访问时,本机的DNS协议会向已经设置的DNS服务器发出请求,完成域名到IP地址的转换,执拗搜索到对应的IP地址。
(2)TCP协议和端口
TCP协议实现可靠通信的基础是采用“握手”机制实现了数据的同步传输,即在通信的双方发送数据前首先建立连接,协商一些参数,如发送的数据字节数量、缓冲区大小等。首先建立连接再传送数据,并且对于收到的每一个分组进行确认,这样就很好的保证了数据的可靠传输。
一个主机可以和服务器上的多个进程保持TCP链接,如主机1访问服务器的Web服务,同时又使用FTP服务下载视频文件,这样主机1和服务器就建立了两个连接。这里对连接的标识显得很重要,因为主机1上的进程需要知道和服务器上的哪个服务进程建立TCP连接。TCP协议提供了端口号的概念,每个端口号对应一个应用进程,如端口号80代表HTTP连接,端口号21代表FTP连接服务。这样TCP协议软件可以通过端口号识别不同的进程。
端口号的设置有一定的限制,最大数是65535,在1024之前是知名端口号,是全世界统一的,如FTP服务进程的端口号是25,HTTP服务进程的端口号是80等。而1024~65535之间由用户自己选择使用。
(3)客户/服务器通信模型
客户/服务器通信模型通常称为C/S模型(Client/Server模型)。这种通信模型中有两个软件主体,一个是客户程序,另一个是服务器程序。我们通常称为客户端和服务器端。通信的过程是客户端向服务器端发送请求,而服务器收到请求后处理请求,把数据返回客户端,从而完成一次通信过程。需要说明的是客户端和服务器端是一个相对的概念,读者需要理解,发出请求的一端为客户端,而接受并处理请求的一端为服务器端。曾经是客户端的主机如果同时向其他机器提供服务,如WWW服务,则该主机相对于发出WWW服务请求的主机来说也是服务器。
2.UDP协议
UDP(User Datagram Protocol)协议称为用于数据报协议。该协议运行在TCP/IP模型的传输层,协议可以直接封装成IP分组,不需要事先建立连接就可以发送这些封装好的IP分组。
一个UDP报文由两个端口(即源机器端口和目的机器端口)、UDP长度和UDP校验和组成。通过目的端口,目的主机的传输层就知道把该报文递交给哪个处理进程,而源端口直到从目标主机返回的UDP报文到达源主机后才可以正确地提交给上层进程处理。
UDP协议不考虑流量控制、差错控制和损坏数据处理,即使收到的是受损的数据也不要求发送端重传。因为它是无连接的协议,所以也不需要事先建立连接,从而节约了建立连接的时间。另外,其传输数据是异步的,使得数据能够及时的发送到网络上,减少了数据处理和传输的时延。
由于UDP协议在传输数据时是不可靠的,如果应用层要求接收正确的数据,就需要做很多工作,如数据乱序、数据丢失等。
由于采用UDP协议是无链接的,所以客户端和服务器是相对的概念。因为二者没有--对应关系,在发送数据前不需要和对方建立链接,所以采用UDP协议通信的双方都可以称为服务器。
3.基于Java的客户/服务器程序
本章介绍的Java网络编程建立在TCP/IP协议基础上,使用Socket套接字编写网络通信程序。使用套接字编程可使程序员把主要精力集中在应用层,集中于需要解决的问题领域,对于传输层和网络层,以及更底层的网络细节则不用考虑,这些问题Socket套接字会全部处理。也就是说使用套接字使程序员看不到底层的通信细节,而只是在应用层使用Socket完成两个主机之间的通信。
(1)Socket及其原语
Socket可以看做一个通信实体,负责完成位于不同主机上的应用程序间通信。该套接字提供了一组原语(一组最基本的操作),保证客户端和服务器端顺利地建立通信链接并传输数据,最后释放链接。下图列出了8个原语:
其中前4个原语由服务器按照表中声明的顺序依次调用。SOCKET调用创建了新的通信端点,即新的套接字,但是此时它没有网络地址,所以调用BIND原语为套接字绑定网络地址,一旦绑定成功,客户端就可以同服务器建立链接。继续调用LISTEN原语,该原语为进来的链接请求建立排队空间。此时的LISTEN调用不会阻塞服务器进程,所以服务器继续调用ACCEPT原语,当客户端发出了一个链接请求,则服务器端停止阻塞,创建与该客户端链接的一个新Socket。此时服务器可以启动一个新线程来处理刚刚建立的客户端与服务器端的Socket链接,而服务器继续等待新的链接。
客户端同样也使用SOCKET原语创建一个套接字,然后调用CONNECT原语负责调用,并主动发起对服务器的链接,一旦CONNECT调用完成,客户进程与服务器建立了链接。此时双方就可以使用SEND和RECV在建立的链接上进行通信。
建立套接字的双方对称的释放链接,允许一方关闭Socket,此时说明它不再发送数据但是可以接收数据,当双方都调用了CLOSE原语后,该通信链接释放。
在Java中,提供了3种套接字类,以满足用户编写网络程序的需要。它们分别是java.net.Socket/java.net.ServerSocket和java.net.DatagramSocket。其中Socket和ServerSocket是建立在TCP可靠链接基础上,而DatagramSocket是建立在不可靠的UDP协议上的。
范例:以一个简单的基于TCP协议的客户服务器示例程序介绍Socket和ServerSocket,使读者对客户/服务器通信模型和Java网络编程有一个直观的感受和初步的理解。
EchoServer服务器示例程序:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
//定义监听端口
public static final int PORT=8080;
public static void main(String[] args) throws IOException{
//建立ServerSocket对象,监听端口号为8080
ServerSocket server = new ServerSocket(PORT);
System.out.println("服务器启动:"+server);
try{
//服务器启动监听,等待客户的链接
Socket socket=server.accept();
try{
System.out.println("建立链接:"+socket);
//获得Socket的输入流,并用BufferedReader包装
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获得Socket的输出流,并用PrintWriter包装
PrintWriter writer = new PrintWriter(socket.getOutputStream(),true);
//使用无限循环在输入流中每次读一行数据,并输出到控制台,同时把数据再写入输出流,返回客户端,如果从输入流中读到“endd”字符串
//则结束循环
while(true){
String string =reader.readLine();
if(string.equals("end")) break;
System.out.println("from Client:"+string);
}
//无论是否发生异常,一旦退出第10行的try区块,则关闭代表服务器的Socket对象
}
finally{
System.out.println("关闭链接");
socket.close();
}
//最后关闭服务器对象
}
//最后关闭服务器对象
finally{
server.close();
}
}
}
代码说明:
服务器一旦启动就停留在如下代码处:
Socket socket=server.accept().
此时服务器程序主阻塞,等待客户端的链接请求,一旦客户端发出链接请求,则上述方法就返回一个Socket对象,接着继续执行程序下面的代码,即建立输入、输出流对象,通过一个无限循环不断读取从输入流缓冲的数据,一次读入一行,把读到的数据打印到控制台,同时把读到的每行数据又写入输出流,送回客户端程序。当客户端发送“end”字符串时,就会跳出无限循环,并按顺序依次执行两个finally子句,执行socket.close()关闭链接,执行server.close()关闭建立SocketServer对象占用的资源(如端口号)。
(2)创建服务器
创建服务器使得服务器同客户端传输数据需要3个基本步骤:
《1》创建ServerSocket对象
创建服务器使用java.net.ServerSocket类。在创建服务器对象时,必须指定一个协议端口,该端口值使得客户端知道需要访问服务器上的哪种服务。在编写服务器程序时,调用ServerSocket类的带参数构造函数创建服务器对象,参数指定服务器的监听端口。
ServerSocket server=new ServerSocket(8080);
用关键字new创建了ServerSocket对象,此时还不能做任何事,只是使操作系统注册了服务器进程,接下来要启动监听、阻塞服务进程。
《2》阻塞服务进程、启动监听
ServerSocket对象调用accept()方法使得服务器进程阻塞,从而启动监听。等待客户端的链接请求,一旦建立一个链接该方法返回一个Socket对象,这个Socket对象与服务器端的Socket对象建立起链接。
Socket socket = server.accept();//启动监听,等待客户链接
一旦建立通信链接双方具备发送和接收数据的准备,服务器端通过输入流读数据,通过输出流向客户端发送数据,所以设计下一个步骤。
《3》创建流并读、写数据
Socket类提供了两个方法getInputStream()和getOutputStream(),前者获得输入流对象,后者获得输出流对象,一旦得到输入、输出流对象,就可以通过过滤流来包装这些流对象。一般使用BufferedReader包装输入流,用PrintWriter包装输出流,调用PrintWriter的对象println()方法,每次向对方发送一行数据,调用BufferedReader类对象的readLine()方法每次读一行数据。
//建立输入流对象,使用该对象从建立链接的Socket对象中读数据
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//建立输出流对象,使用该对象向建立了链接的Socket对象中写数据
PrintWriter writer=new PrintWriter(socket.getOutputStream(),true);
(3)创建客户端
创建客户端程序与创建服务器程序略有区别,显然服务器需要监听服务请求,而客户端只需要启动链接请求,所以创建客户端的关键是创建一个Socket对象。下面的代码是Client客户程序中创建Socket对象的过程。
private static String serverAddress="Iocalhost";
private static int port=8080;
Socket socket=new Socket(serverAddress,port);
在Socket类的构造函数中有两个参数,第一个为服务器的主机名,第二个为服务器进程的服务端口。这里我们把服务器和客户端运行在同一台主机上,而主机的默认主机名就是
localhost。所以采用这种方式在缺少网络环境的情况下,可以在一台主机上分别运行服务器程序和客户端程序做测试。
客户程序一旦通过new关键字创建了Socket对象(前提是服务器已经启动),则表明已经成功建立链接,TCP通过地址和端口号来识别应用层服务,即上述创建的Socket对象
同本机上的一个服务端口为8080的服务程序建立了链接。随后,客户端通过输入输出流来读数据和写数据。
范例:下面代码为Client客户端示例程序,演示了创建Socket的整个过程,该客户端程序一旦启动,则等待用户在控制台输入数据。如有数据,按“Enter”键数据被发送到服务器
端,同时把服务器返回的数据打印在控制台上显示
Client客户端示例程序:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
//定义服务器主机名
private static String serverAddress="localhost";
//定义服务器服务端口,注意不能使用1024以内的数字
private static int port=8080;
public static void main(String []args) throws IOException {
//创建Socket对象
Socket socket = new Socket(serverAddress,port);
try{
//输出socket信息,包括主机地址和端口号等信息
System.out.println("socket="+socket);
//获得socket对象的输入流,并用Bufferedeader缓冲,以便于调用readLine()方法一次一行的读入输入流的数据
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//获得输入流
PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
//true指每写一行数据,就清空缓存
//读入控制台输入的数据
BufferedReader localreader = new BufferedReader(new InputStreamReader(System.in));
String msg=null;
while((msg=localreader.readLine())!=null){
//把每次读到的数据写入输出流中
writer.println(msg);
//从输入流中读取服务器返回的数据
System.out.println("from server:"+reader.readLine());
//如果控制输入的“end”则跳出while循环,断开链接
if(msg.equals("end"))
break;
}
}catch(IOException ex){
ex.printStackTrace();
}finally{
System.out.println("关闭链接");
//因为关闭Socket时,也可能发生异常,所以使用try/catch处理
try{
socket.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
}
}
(4)Socket类详解
Socket套接字在客户、服务器通信模型中扮演着十分重要的角色,它就如同服务器和客户端的代理,完成双方应用程序发送和接收数据的任务。客户建立和服务器的链接创建了
Socket,服务器一旦接收客户的链接请求也返回一个Socket,之后两个Socket就可以收发数据了。
《1》创建Socket
Socket类的几种构造函数如下,其区别在于参数的不同,使用户创建Socket对象更加灵活
1>Socket()
该构造方法没有任何参数,创建没有建立链接的Socket对象,该对象包含系统的默认属性。
2>Socket(InetAddress address,int port) throws IOException
该构造函数创建一个流Socket,并且链接到指定IP地址的指定端口。其中第一个参数是服务器主机的IP地址,第二个参数是服务器进程的服务端口
3>Socket(String host,int port) throws UnkownHostException,IOException
该构造函数创建一个流Socket,并且链接到指定主机的指定端口。如果指定的主机名为null。相当于通过InetAddress.getByName(null)获得的地址(即localhost),其中第一个参数是服务器主机的名字,第二个参数是服务器进程的服务端口。
4>Socket(InetAddress address,int port,InetAddress localadd, int localport) throws IOException
该构造函数创建一个Socket对象,链接到指定的远程地址的指定端口。该Socket还将绑定到本地的IP地址和本地的客户程序服务端口。函数中有四个参数,前两个参数是远端服务器的IP地址和服务端口号,后两个参数是本地客户端的IP地址和客户进程端口号。
5>Socket(String host,int pot,InetAddress localadd,int localport) throws IOException
该构造函数创建一个流Socket,并且链接到指定名字的主机的指定端口。如果指定的主机名为null,相当于通过InetAddress.getByName(null)获得的地址(即localhost),同时该方法绑定本地主机的IP地址和服务端口号。
根据上述5个构造函数的介绍,可以了解到除了Socket()函数创建一个不带链接的Socket对象,其他4个构造函数都返回一个Socket对象,该对象知道包含丰富的建立Socket通信的信息,如远端服务器的IP地址、服务进程的端口号、本地主机的IP地址和本地客户进程的端口号。
下面分别介绍这些构造函数的使用,将其分为3类:第一类为创建无链接的Socket,第二类为绑定服务器,第三类为绑定客户端。
1>创建无链接的Socket
用户在使用关键字new创建Socket对象时,可以创建一个无链接的Socket对象。如果用户需要访问远端服务器进程,可以调用该Socket对象connect()方法实现,并可以设置其他参数。下面的代码说明其使用方式:
//创建不带参数的Socket对象
Socket socket=new Socket();
//创建类InetSocketAddress的对象,该类是SocketAddress的直接子类
SocketAddress serverAddress=new InetSocketAddress("localhost",8080);
//调用Socket类的connet()方法建立到服务器的链接,该方法不返回任何值(void)
socket.connect(serverAddress);
InetSocketAddress类是SocketAddress类的直接子类,它的构造函数接收一对参数,即主机名+端口号。此时的主机名和端口号是针对远程服务器而言的。而Socket类的connet()方法接收一个InetSocketAddress类对象为参数,建立Socket链接。
其中还有一种connect()方法,接收两个参数,其定义是:
public void connect(SocketAddress endpoint,int timeout) throws IOException
这个方法在socket服务器间建立链接,并且设定了超时间隔,如果超时间隔设置为0,则解释为无限等待。此时该进程被阻塞直到建立链接或发生错误。如果因为底层网络的影响使得链接建立的时间很长,则可以设置链接超时间隔参数,该参数的计数单位为毫秒,使得系统等待有限的链接时间,如2分钟(120000毫秒),修改代码如下:
Socket socket=new Socket();
SocketAddress serverAddress=new InetSocketAddress("localhost",8080);
//调用Socket类的connet()方法建立到服务器的链接,等待时长2分钟
socket.connect(serverAddress,120000);
2>绑定服务器
这类构造函数只绑定服务器的信息。通过已知的服务器IP地址或主机名和相应的服务器进程的服务端口号,绑定服务器主机。这类构造函数是:
Socket(InetAddress address,int port);//第一个参数是服务器的IP地址
Socket(String hostname,int port);//第一个参数是服务器的主机名字
InetAddress类表示服务器的IP地址,该类的定义是:
public class InetAddress extends Object implements Serializable
该类提供了两个重要静态方法用来构造该类的对象
Static InetAddress getByName(String host) //返回指定主机名的相应的IP地址
Static InetAddress getLocalHost()//返回本地主机地址
测试InetAddress类的静态方法示例程序:
import java.io.IOException;
import java.net.InetAddress;
public class InetTest {
public static void main(String[]args) throws IOException{
//返回主机名为localhost的IP地址,"localhost"可以认为是域名
System.out.println(InetAddress.getByName("localhost"));
//getByName(null)的参数为null,自动使用参数为"localhost"
System.out.println(InetAddress.getByName(null));
//返回本地主机的IP地址,此时InetAddress对象信息可能包含IP地址和相应的主机名
System.out.println(InetAddress.getLocalHost());
//返回代表"127.0.0.1"的IP地址
System.out.println(InetAddress.getByName("127.0.0.1"));
}
}
运行结果:
localhost/127.0.0.1
localhost/127.0.0.1
ty-201603121107/192.168.56.1
/127.0.0.1
代码说明:
InetAddress类为在没有网络环境下测试客户、服务器程序提供了方便。InetAddress.getByName(null),该调用返回一个InetAddress类对象,指明本地机器。也可以调用InetAddress.getByName("localhost")获得本地回路地址(在hosts文件中有主机名和IP地址的对应关系)
3>绑定客户端
在构造Socket对象时,一般情况下我们不显式设置客户端的IP地址和服务端口号。而是采用默认值。IP地址由客户端所在的主机决定,而客户服务的端口号由操作系统随机分配。例如指定了客户主机与服务器通信,此时可以采用显式地设置客户端的IP地址和客户进程端口。Socket类提供了两个构造函数实现客户端的绑定。
Socket(InetAdderss address,int pot,InetAddress localAddress,int localport);
Socket(String hostname,int port,InetAddress localAddress,int localport);
虽然客户端显式绑定IP地址和端口号的做法很少用,但是必定有使用场合。如果配有多块网卡的主机分别连在不同的网络上,如一个链接在Internet上,而一个链接在本地局域网上,此时如果客户程序需要访问局域网中的服务器,就需要显式地绑定客户端了。假设客户机器的IP地址是23.9.1.109,使用端口1234,而服务器IP地址是23.9.1.145,服务端口是8080,则可以如下构造Socket对象。
InetAddress clientAddress=InetAddress.getByName("23.9.1.109")
InetAddress serverAddress=InetAddress.getByName("23.9.1.145")
Socket (serverAddress ,8080,clientAddress,1234);
(2)Socket类的getXX()方法
Socket类提供了getXX()方法,使用这些方法可以获得Socket对象的丰富信息,如建立Socket链接必需的主机IP地址和进程端口号。一旦建立了链接,Socket提供了输入输出流方法来读取服务的数据,接收从服务器返回的数据。下面详细介绍经常使用Socket类的各种getXX()方法。
public InetAddress getInetAddress():该方法返回建立了Socket链接的远端Socket的地址,如果没有建立链接则返回null
public InetAddress getLocalAddress();该方法返回建立了Socket链接的本地地址。
public int getPort():该方法返回建立了Socket链接的远端服务的端口号
public int getLocalPort():该方法返回建立了Socket链接的本地服务端口号
public InputStream getInputStream() throws IOException:该方法返回当前Socket的输入流
public OutputStream getOutputStream() throws IOException:该方法返回当前Socket的输出流
测试Socket类的getxx()方法:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
public class SimpleClient {
static Socket server;
public static void main(String[]args) throws IOException{
//创建socket对象sever,服务器地址为本地主机地址,服务端口号为8080
server=new Socket(InetAddress.getLocalHost(),8080);
System.out.println("建立Socket链接......");
//通过Socket对象server获得本地客户进程端口
System.out.println("本地服务端口:"+server.getLocalPort());
//通过Socket对象server获得远端服务器的服务进程端口
System.out.println("服务器端口:"+server.getPort());
//获得本地客户端的地址信息
System.out.println("本地地址信息:"+server.getLocalAddress());
//获得远端服务器的地址信息
System.out.println("服务器InetAddress对象信息:"+server.getInetAddress());
//获得远端服务器的主机IP地址
System.out.println("服务器的ip地址:"+server.getInetAddress().getHostAddress());
//获得远端服务器主机的主机名
System.out.println("服务器的主机名:"+server.getInetAddress().getHostName());
BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
PrintWriter out=new PrintWriter(server.getOutputStream());
BufferedReader wt=new BufferedReader(new InputStreamReader(System.in));
while(true){
String str= wt.readLine();
out.write(str);
out.flush();
if(str.equals("end")){
break;
}
System.out.println(in.readLine());
}
server.close();
}
}
(3)Socket类的setXX()方法
1>public void setTcpNoDelay(Boolean on ) throws IOException
在系统默认情况下发送数据采用了Nagale算法,采用这种算法在发送数据时,会把要发送的数据首先存储在缓冲区中,直到缓冲区满后才发送出去。显然这种算法对于发送大量数据是高效的,因为使用缓冲区会显著减少发送数据的次数。但是如果发送的数据量很少,而且对实时性要求很高的应用就不应该使用这种算法,如何屏蔽系统的这种默认方式,就需要调用setTcpNoDelay()方法,设置该类的TCP_NODELAY选项,决定是否使用Nagale算法。如果调用setTcpNoDelay(true)方法即可关闭Socket的缓冲区,保证数据及时发送出去。如下代码所示:
Socket socket=new Socket("remotehost",8080);
socket.setTcpNoDelay(true);
2>public void setReuseAddress(boolean on) throws SocketException
当关闭TCP链接时,在关闭链接后,原来的链接会保持一段时间在超时状态,或称为超时等待状态,在这种超时等待状态下的链接占用了well-known的socket地址或端口,那么把该地址或端口再绑定到另一个socket可能会出现问题
为了确保一个Socket通信进程关闭后,即使处于超时等待状态,同一个主机上的其他Socket进程可以立即绑定到该端口,调用setReuseAddress(true)方法来设置该类的SO_REUSEDDR选项。如下代码所示:
//创建未建立链接的socket对象
Socket socket=new Socket();
socket.setReuseAddress(true);
//创建本地客户InetSocketAddress对象
SocketAddress localAddr=new InetSocketAddress("localhost",5678);
//创建服务器端InetSocketAddress对象
SocketAddress remoteAddr=new InetSocketAddress("remotehost",8080);
//绑定本地端口
Socket.bing(localAddr);
//建立到服务器的链接
Socket.connect(remoteAddr);
注意:
当创建Socket对象时,默认选项SO_REUSEDDR的默认值是关闭的(disabled),并且方法setReuseAddress(true)必须在socket没有调用bind(SocketAddress)之前调用才有效。如果在调用方法socket.setReuseAddress(true)时发生异常,或socket关闭会抛出SocketException。
3>public void setSoTimeout(int timeout) throws SocketException
该方法用于设置Socket接收数据的等待时间,单位是毫秒,该方法必须在读数据前被调用且时间值必须大于0,0值标识等待时间不受限制,这也是创建Socket对象时的默认设置。如下代码所示:
socket.setSoTimeout(60000);
byte[]buff=new byte[1024];
InputStream in =new socket.getInputStream();
in.read(buff);
如果程序的read()方法在等待1分钟后还没有收到数据,就抛出SocketTimeoutException,此时Socket仍然是链接的,会再次尝试读入数据,或继续等待。
测试超时的MyServer示例程序:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class MyServer {
public static void main(String[] args) throws IOException {
ServerSocket mysocket=new ServerSocket(8080);
Socket socket= mysocket.accept();
//设置读数据的等待时间间隔为10秒,如果在10秒钟内没有收到数据则抛出超时异常
socket.setSoTimeout(10000);
InputStream in =socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try{
while(true){
//调用readLine()读数据时,可能抛出超时异常
String s= reader.readLine();
if(s.equals(null)) break;
System.out.println(s);
}
}//捕获超时异常,并打印消息
catch(SocketTimeoutException ex){
System.out.println("读数据超时");
}
}
}
注意:这里一旦客户端和服务器建立起Socket,则执行读数据超时设置,该设置必须在读数据之前执行,否则无效
测试超时的MyClient示例程序:
package LianXi;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
public class MyClient {
public static void main(String[] args) throws IOException {
Socket socket=new Socket("localhost",8081);
PrintWriter writer=new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
try{
//客户端向输入流写入一个字符串,之后线程休眠20秒,此时服务器无法接受数据
writer.println("how are you !");
Thread.sleep(20000);
}//捕获InterruptedException异常,因为在线程休眠期间会发生被异常中断的事件
catch(InterruptedException ex){
System.out.println("interrupted exception");
}
//线程休眠中恢复时,或发生异常后则关闭Socket流
socket.close();
}
}
代码说明:在线程休眠的20秒内,服务器端会触发读数据超时异常,先后执行MyServer和MyClient程序,执行结果如下:
how are you !
读数据超时
4>public void setSoLinger(boolean on,int seconds) throws SocketException
在Socket通信过程中会发生这样的情况,关闭Socket时,即调用close()方法时仍有未发送完毕的数据,所以Java的Socket机制并不是停止数据的发送,而是持续一段时间等发送完所有的数据时才真正关闭了Socket通信,这里发送完的数据指发送到网络上去的但没有得到确认的数据(保证数据的可靠传输)。
而使用setSoLinger(boolean on,int seconds)方法可以实现立即关闭Socket通信,而不考虑未发送完的数据。如下代码所示:
Socket.setSoLinger(true,0)
Socket.setSoLinger(true,30)
前者表示立即关闭Socket通信,后者表示无论数据是否发送完毕,在30秒后也会关闭Socket通信。
简单的服务器示例程序:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class LingerTestServer {
public static void main(String[] args)throws IOException,InterruptedException {
ServerSocket server =new ServerSocket(8082);
Socket s=server.accept();
Thread.sleep(5000);
InputStream in=s.getInputStream();
ByteArrayOutputStream buffe = new ByteArrayOutputStream();
byte[]buff=new byte[1024];
int len=-1;
do{
len=in.read(buff);
if(len!=-1)
buffe.write(buff, 0, len);
}while(len!=-1);
System.out.println(new String(buffe.toByteArray()));
}
}
注意:
这里一旦客户端和服务器建立起Socket,则执行读数据超时设置,该设置必须在读数据之前执行,否则无效。
简单的客户端示例程序:
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class LingerTestClient {
public static void main(String[] args) throws IOException {
Socket s=new Socket("localhost",8082);
//s.setSoLinger(true, 0);
//s.setSoLinger(true, 1000);
s.setSoLinger(true, 5);
OutputStream out=s.getOutputStream();
StringBuffer sb=new StringBuffer();
for(int i=0;i<10000;i++)
sb.append(i);
out.write(sb.toString().getBytes());
System.out.println("starting close socket");
long begin=System.currentTimeMillis();
s.close();
long end=System.currentTimeMillis();
System.out.println("关闭socket所用时间为:"+(end-begin)+"毫秒");
}
}
5>public void setReceiveBufferSize(int size) throws SocketException
显然该方法是设置接收数据的缓冲区大小,缓冲的目的是减少数据的发送次数,提高通信效率。如果是大数据量,则采用大的缓冲区,一次接收更多的数据,使得传输信道上尽可能的充满数据,这样就可以提高线路的利用率和系统的吞吐量;而如果是小数据量,则应该尽量使用小的缓冲区,这样可以减少等待时间。如代码所示:
Socket.setReceiveBufferSize(1024);
6>public void setSendBufferSize(int size) throws SocketException
该方法设置发送缓冲区的大小,如果是大数据量,则需要设置相对较大的缓冲区,这样一次就可以发送更多的数据,提高线路利用率。对于小数据量,选择较小的缓冲区以减少接收方的等待时间。如下代码所示:
Socket.setSendBufferSize(1024);
7>public void setPerformance(int connectionTime,int latency,int bandwidth)
在Socket通信过程中,不同的应用对于系统性能有不同的需求,如对于链接时间、延迟和线路带宽等需求各不相同,Socket类提供了该方法用于设置在这些性能指标之间作出相对重要性选择。其中参数的int型值大小表明重要性的顺序,即哪个数值大,就表示其性能最重要,如下所示:
Socket.setPerformance(1,3,2);这表明该通信对于通信延迟要求最高。
(4)关闭Socket
建立Socket通信的双方一旦通信结束,需要及时关闭Socket,这样就可以及时释放链接占用的资源,如端口号、绑定的IP地址。Socket对象调用close()方法关闭Socket通信,此时对象的输入输出流不再进行输入输出操作。
如需要立即释放服务端口和IP地址,需要事先调用SetReuseAddress(boolean on)方法。如下代码所示:
socket.setReuseAddress(true);
如果需要立即抛弃未发送完毕的数据,需要事先调用setSoLinger(boolean on,int time)方法。如下代码所示:
socket.setSoLinger(true,0)
注意:
在编写网络程序时,务必保证关闭Socket,所以最好使用finally子句实现关闭Socket通信的目的,以释放被占用的资源。
5.SocketServer类
(1)创建SocketServer
Java提供了灵活的构造函数来创建SocketServer对象,该对象负责在服务器端监听客户端请求,下面依次介绍构造函数和使用方式。
1>public void ServerSocket() thows IOException
该构造函数创建一个不带参数的默认构造方法。显然该构造函数不与然和端口绑定,这样创建的对象无法直接调用accept()方法类监听接入的访问,必须绑定一个服务端口,客户端程序才可以访问到服务器程序。
ServerSocket类提供了bind()方法来绑定特定的SocketServer对象。如下代码所示:
private int serverPort=8080;
SocketServer serverSocket = new SocketServer();
serverSocket.bind(serverPort);
该构造函数还有一个用途就是在服务器与特定端口绑定之前,设置一些服务器参数,如设置SocketServer类的setReuseAddress选项为true,再绑定服务端口,如下代码所示:
private int serverPort=8080;
serverSocket.setReuseAddress(true)
serverSocket.set......();
serverSocket.bind(serverPort);
注意必须在绑定端口的那行代码前设置SocketServer类的参数,否则该设置参数不起作用。
2>public ServerSocket(int port) throws IOException
该构造函数创建一个带端口参数的ServerSocket对象,默认的服务器地址为本机的IP地址。如下代码所示:
private int serverPort=8080;
SocketServer serverSocket = new SocketServer(serverPort);
一旦创建该服务器端ServerSocket对象,就绑定端口8080,采用本机的IP地址作为客户端链接服务器的地址。
注意如果该服务端口被其他进程占用,则会抛出IOException异常
3>public void ServerSocket(int port,int connectNumber) throws IOException
该构造函数有两个参数,第一个参数指定服务器的服务端口,第二个参数指定服务器管理链接请求的数量。如下代码所示:
private int serverPort=8080;
SocketServer serverSocket=new SocketServer(serverPort,10);
.......
serverSocket.accept();
该构造函数设置了客户端链接请求的队列长度,如果有多于10个客户端向服务器发出链接请求,则服务器丢弃该请求。
在客户端向服务器发出链接请求后,该链接请求由操作系统负责,操作系统采用一个FIFO(先进先出)队列管理该链接请求,一旦请求的数量超过一定的数量,如上述代码设置了10个链接,则拒绝接下来的链接请求。
ServerSocket的accept()方法会从链接请求队列中取出链接请求,对于单线程而言,在服务器响应客户端请求后,就返回继续从请求队列中取出链接请求这样空出的队列位置可以接受更多客户端的请求。
服务器中有如下代码:
private int port=8080;
SocketServer serverSocket=new SocketServer(port,5);
......
//serverSocket.accept();
客户端有如下代码:
private int port=8080;
for(int i=0;i<20;i++){
New Socket("localhost",port);
}
如果首先启动服务器,注释掉代码serverSocket.accept(),再启动客户端,此时客户端试图与服务器进行20次链接,但是服务器设置的最大请求链接数量是5,所以在5次成功链接后,客户端会抛出java.net.ConnectException异常
如果取消代码serverSocket.accept()的注释,服务器会不断从管理链接请求的队列中取出请求,所以允许更多客户端请求,再启动服务器和客户端,就不会抛出异常。
4>public void ServerSocket(int port,int connectNumber,InetAddress address) throws IOException
该构造函数的第一个参数是服务端口号,第二个参数是链接请求的最大数量,第三个参数是服务器要绑定的IP地址
private int port=8080;
InetAddress address=InetAddress.getByName("localhost");
ServerSocket serverSocket = new ServerSocket(port,20,address);
说明:一台机器链接在两个网络上,如果该服务器仅仅被其中一个网络上的主机访问,就需要这种显式地设置服务器地址的方式。
(2)SocketServer类的两个重要方法
1>SocketServer类accept()方法
当客户端向服务器发出请求时,该请求保存在服务器主机操作系统维护的请求队列中,accept()方法负责从该请求队列中取出一个最早的请求,该方法返回一个Socket对象,服务器通过该对象实现与客户端的双向可靠通信。
服务器通过该Socket对象获得输入、输出流。通过输入流读取客户发来的数据,通过输出流向客户端发送数据。一旦通信完毕,则服务器程序会继续监听,并阻塞进程,瞪大新的客户链接。
如果服务器端采用多线程程序,使得和客户交互的代码放入一个独立的线程。此时一旦调用accept()方法返回一个Socket对象,服务端会复制一个Socket对象和客户端通信,accept()方法不会阻塞进程,而是继续监听客户的链接请求。
2>SocketServer类close()方法
该方法关闭当前服务器和客户端的所有链接,释放链接占用的资源如服务端口。一般情况下不需要显式的调用该方法,服务器程序退出时,操作系统会自动释放端口资源。显式关闭SocketServer对象的方式如下所示:
ServerSocket serverSocket=new ServerSocket(8080);
serverSocket.close();
和关闭ServerSocket链接相关的两个方法:
public Boolean isClosed():该方法用于判断ServerSocket是否关闭,如果关闭该方法返回true,否则返回false.
public Boolean isBound();该方法用于判断ServerSocket是否已经绑定到指定的服务端口,这个端口是必须绑定的。如果已经与相应的端口绑定,该方法返回true,否则返回false
(3)读取SocketServer信息
ServerSocket类提供了一系列的get方法来获得ServerSocket的信息,如下所示:
public InetAddress getInetAddress();该方法返回建立了Socket链接的远端Socket的地址,如果没有建立链接则返回null
public InetAddress getLocalAddress();该方法返回建立了Socket链接的本地地址。
public int getPort();该方法返回建立了Socket链接的远端服务的端口号。
public int getLocalPort();该方法返回建立了Socket链接的本地服务端口号
public InputStream getInputStream() throws IOException;该方法返回当前Socket的输入流
public OutputStream getOutputStream() throws IOException;该方法返回当前Socket的输出流。
其中常用的两个方法如下:
1>public InetAddress getInetAddress();该方法返回该服务器绑定的IP地址,如果创建ServerSocket对象时采用默认的地址,则是服务器的本地地址;如果在创建对象时显式指定了服务器地址,则getInetAddress()方法返回的是指定的地址。
2>public int getLocalPort();该方法返回服务端口,如果用户没有显式设置该端口,则操作系统会随机分配一个端口,否则返回用户指定的端口。
获得ServerSocket端口和地址信息的示例程序:
import java.io.IOException;
import java.net.*;
public class GetPortAndAddTest {
public static void main(String[] args) throws IOException {
//ServerSocket serverSocket=new ServerSocket(8080);
ServerSocket serverSocket =new ServerSocket(0);
System.out.println("监听端口:"+serverSocket.getLocalPort());
System.out.println("服务器地址:"+serverSocket.getInetAddress());
}
}
注意:
一般服务器的监听端口应该设置固定的值,因为客户端发出建立链接请求前需要事先知道服务器端的监听端口
ServerSocket serverSocket =new ServerSocket(0);
执行这个语句,则服务器监听端口是由操作系统随机分配,地址默认为服务器的本地地址。所以每次执行输出的端口是不一样的。
6.数据报通信
在TCP/P协议族中,UDP协议与TCP协议都处在传输层。UDP协议是一种无链接协议,即建立通信的双方无需事先建立联系,如果需要发送数据即发送,不需要考虑对方是否接受或网络是否可靠。它发送的每个数据报称为UDP报文,也称为UDP数据报。每个数据报相互独立,各自包含完整的目的地地址、源地址和相应的端口号。数据报在网络的传输路径取决于网络自身的状况,至于能否到达目的地或到达后报文的正确性却难以保证。在对方收到UDP报文后也不会作出任何反馈告诉发送方当前的状态,显然这种方式即无法保证数据的正确性,也无法处理丢失报文的情况。
UDP协议是无链接的、不可靠的传输层协议,而TCP是面向链接的、可靠的传输层协议。
UDP协议无需事先建立链接而直接发送数据,节省了建立链接的时间。TCP协议是面向链接的协议,所以在通信双方通信前首先建立Socket链接,协商一些参数,如报文序列号、缓冲大小等。所以建立TCP链接首先要付出初始建立链接的时间开销。
UDP协议在传输数据时对报文大小有严格的要求,每个被传输的UDP报文的大小限制在64KB以内。而TCP协议对报文大小没有要求,一旦大报文发送出去,由网络层负责切分成小的分组,这一点对传输层是不可见的。
UDP协议是不可靠的传输协议,体现在不保证报文异动到达对方,也不保证报文到达的顺序与发送方相同。而TCP协议可以很好地保证报文按照 顺序可靠地到达接收方。
(1)数据报通信简介
基于Java的数据报通信主要依靠两个类来完成:一个是java.net.DatagramSocket类,一个是java.net.DatagramPacket类。其中DatagramPacket表示要发送或接收的数据报,
而DatagramSocket负责接收和发送数据报。
//创建一个数据报
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
//等待接收数据报,如果没有接收到数据,进程阻塞
socket.receive(packet);
//创建要发送的数据报
DatagramPacket packet=new DatagramPacket(outputData,outputData.length,remoteIP,8080);
//发送数据报
socket.seng(Packet);
每个DatagramSocket与本地IP地址和端口号绑定,可以发送数据报给任何一个远端DatagramSocket对象,也可以接收任何远端的DatagramSocket独享发送的数据报。注意,此时没有像Socket通信那样首先建立一个链接,这种通信方式也称为异步通信方式,通信前双方不需要事先协商。在这样的通信过程中,每个UDP报文包含了远端IP地址、端口号信息。
(2)DatagramPacket类
1>构造函数
使用UDP协议发送和接收数据,需要在程序中表示数据报以调用DatagramSocket的receive()和send()方法来接收和发送数据报。DatagramPacket对象就是程序中的数据报,该类通过构造函数创建不同的数据报。其构造函数分为两类,一类创建用于发送的数据报,一类用于接收的数据报。用于接收的数据报的构造函数如下:
。。。public DatagramPacket (byte[] buff,int length);
该函数创建用于接收的数据报,第一个参数是字节数据类型的对象,用于防止接收的数据,第二个参数指定需要接收的字节数,如果接收到的数据报的字节数比length大,则多于的数据被抛弃。下面代码是创建用于接收数据报的例子:
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
socket.receive(packet);
该DatagramPacket对象把接收的数据放入大小为1024字节的byte型数组中,最大接收的数据报大小为1024字节
。。。public DatagramPacket(byte[]buff,int offset,int length);
该函数创建用于接收的数据报,第一个参数是字节数据类型的对象,用于放置接收到的数据,第二个参数指明把接收的数据放入buff的起始位置,第3个参数指定需要接收的字节数,即一次可以读入的数据报长度值,显然offset的值要小于或等于length的值,下面的代码是创建该类型数据报的例子
DatagramPacket packet=new DatagramPacket(new byte[1024],0,1024);
socket.receive(packet);
该DatagramPacket对象把接收的数据放入大小为1024字节的byte型数组中,从存放数据的数组的第一个位置开始,最大接收的数据报大小为1024字节。
用于发送的数据报的构造函数与用于接收的数据报的构造函数的区别是:前者需要设置数据报的目的地址和端口号,而后者不需要。因为发送出去的数据报需要知道目的地址才能通过网络把数据发送到目的地,而只有端口信息才可保证目的地主机的传输层把数据报递交给合适的服务器进程(接收该数据报的进程)
下面是用于发用的数据报的构造函数:
。。。public DatagramPacket(byte[]buff,int offset,int length,InetAddress address,int port);
该构造函数创建用于发送的数据报,第一个参数buff存放要发送的数据,第二个参数指明要发送的数据在buff中的起始位置,第3个参数length指明要发送的字节数,第4个参数是目的地地址,第5个参数是服务端口号。创建示例代码如下:
private int remotePort=8080;
InetAddress remoteAddress=InetAddress.getByName("localhost");
//获得字符串的字符编码,存入字节数组data中
byte[] data="message".getBytes();
DatagramPacket packet=new DatagramPacket(data,0,data.length,remoteAddress,remotePort);
。。。public DatagramPacket(byte[]buff,int offset,int length,SocketAddress address);
该构造函数用于创建发送的数据报,第一个参数buf存放要发送的数据,第二个参数指明要发送的数据在buff中的起始位置,第3个参数length指明要发送的字节数,第4个参数是目的地地址。创建示例如下代码所示:
private int remotePort=8080;
InetAddress remoteIp=InetAddress.getByName("localhost");
SocketAddress remoteAddress = new InetSocketAddress(remoteIp,remotePort);
//获取字符串的字符编码,存入字符数组data中
byte[]data="message".getBytes()
DatagramPacket packet=new DatagramPacket(data,0,data.length,remoteAddress);
。。。public DatagramPacket(byte[]buff,int length,InetAddress address,int port);
该构造函数创建用于发送的数据报,第一个参数buff存放要发送的数据,第二个参数length指明要发送的字节数,第3个参数是目的地地址,第4个参数是目的端口号。创建示例如下代码所示:
private int port=8080;
InetAddress remoteIp=InetAddress.getByName("localhost");
//获取字符串的字符编码,存入字节数组data中
byte[] data="message".getBytes()
DatagramPacket packet=new DatagramPacket(data,data.length,remoteAddress,port);
。。。public DatagramPacket(byte[]buff,int length,SocketAddress address);
该构造函数创建用于发送的数据报,第一个参数buff存放要发送的数据,第二个参数length指明要发送的字节数,第3个参数是目的地地址。创建示例如下代码所示:
private int remotePort=8080;
InetAddress remoteIp=InetAddress.getByName("localhost");
SocketAddress remoteAddress=new InetSocketAddress(remoteIp,remotePort);
//获得字符串的字符编码,存入字节数组data中
byte[] data="message".getBytes();
DatagramPacket packet=new DatagramPacket(data,data.length,remoteAddress);
2>DatagramPacket类的两个重要方法
。。。public byte[] getData();
当服务器接收到数据报后,接收方的DatagramSocket对象在调用receive()方法接收该数据报,在调用DatagramPacket类的构造函数时,知道如何构建用于接收的数据报,此时用于接收的数据报调用getData()方法,从它的缓冲区中读取字节数据。
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
socket.receive(packet);
//把字节数组转换为字符串,其中packet.getData()返回byte[]类型数据
String msg=new String(packet.getData(),0,packet.getLength);
。。。public void setData(byte[]data);
当客户端发送数据报时,发送方的DatagramSocket调用send()方法发送该数据报,在调用DatagramPacket类的构造函数时,知道如何构造用于发送的数据报,此时用于发送的数据报调用setData()方法,向它的缓冲区中写入字节数据。
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
.......
//通过getByte()方法把字符串转换成byte[]数据乐行,把发送的数据放入packet
packet.setData(("from Server:"+msg).getBytes());
//发送数据报
socket.send(packet);
该类还有一个方法setData(byte[]data,int offset,int length),该方法的第二个参数从data的第offset位置起读数据,读数据的长度为length个字节。调用方法setData(data),相当于调用setData(data,0,data.length).
(3)DatagramSocket类简介
DatagramSocket类负责接收和发送数据报,每个DatagramSocket对象会绑定一个服务端口,这个端口可以是显式设置的,也可以采用匿名端口,匿名端口由操作系统随机分配。UDP数据报在两个DatagramSocket的对象实体间传输。
编写数据报方式的客户、服务器程序时,首先需要在客户方和服务器方建立一个DatagramSocekt对象,用来接收或发送数据报。接收或发送的数据报由DatagramPacket类构造。
1>DatagramSocket类的构造函数
。。。public DatagramSocket() throws SocketException
该构造函数创建一个数据报套接字,绑定到本地主机上的任意一个不被占用的可选端口。
。。。public DatagramSocket(int port) throws SocketException
该构造函数创建一个指定服务端口的数据报套接字,如果该套接字不能被打开或绑定的端口被占用,则抛出SocketException异常。如创建绑定在端口8080的数据报套接字对象:DatagramSocket socket=new DatagramSocket(8080);
。。。public DatagramSocket(SocketAddress address) throws SocketException
该构造函数创建一个数据报套接字,绑定到本地地址,如果本地地址为null,则创建一个未绑定的socket。使用该构造函数创建两个DatagramSocket对象。
SocketAddress address1=new InetAddress("localhost");
DatagramSocket socket1=new DatagramSocket(address1);
SocketAddress address2=new InetAddress("192.168.1.112");
DatagramSocket socket2=new DatagramSocket(addres2);
。。。public DatagramSocket(int port,InetAddress address) throws SocketException
该构造函数创建一个数据报套接字,绑定到一个指定的本地地址和指定的端口。如果一个主机有多个网卡,接到不同的网络上,这种情况就需要该方法来构造数据报套接字。如果绑定的端口被占用则抛出SocketException异常。举例如下:
private int port=8080;
SocketAddress address=new InetAddress("localhost");
DatagramSocket socket=new DatagramSocket(port,address);
2>DatagramSocket的两个重要方法
。。。public void send(DatagramPacket p) throws IOException
该方法负责从当前的socket发送数据报,这些数据报包含了远端主机IP地址,本地和对端的服务端口号等信息。该方法的调用很简单,只要创建了用于发送的数据报对象,把该对象作为函数参数调用即可。如下代码所示:
private int port=8080;
SocketAddress address=new InetAddress("localhost");
byte[]outdata="message".getBytes();
DatagramPacket packet = new DatagramPacket(outdata,outdata.length,address,port);
socket.send(packet);
。。。public void receive(DatagramPacket p) throws IOException
该方法负责接收数据报。该方法的调用也很简单,只要创建了用于接收的数据报对象,把该对象作为函数参数调用即可。如下代码所示:
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
//等待接收数据,如果没有接收到数据,进程阻塞
socket.receive(packet);
该方法的调用会阻塞当前进程,直到收到数据报为止。参数packet用于存放接收到的数据报,该参数设置了缓冲区大小为1024字节,如果收到的数据报大于1024字节,会造成数据的丢失。所以在设置该缓冲区时要尽量设置大一些,以防止数据丢失。
DatagramSocket类还有很多方法可以调用,这里进行简单的说明:
《1》void bind(SocketAddress addr);
把当前的DatagramSocket绑定到具体的地址或端口
《2》void close();
关闭数据报Socket();
《3》void connect(InetAddress addr,int port):该方法保证该Socket只和参数中指定的目的地主机进行UDP报文的发送和接收,这相当于限制了该Socket的通信对象,如果是其他主机发送来的UDP报文,则不接受
《4》void connect(SocketAddress addr);功能同上述方法,不过这里的参数是SocketAddress对象
《5》void disconnect():该方法断开当前Socket链接,此时DatagramSocket对象可以再次链接其他主机,进行UDP报文的传输。
《6》void getBroadcast():检查是否启用了SO_BROADCAST
《7》void getInetAddress();该方法返回DatagramSocket所链接的远程主机的地址,如果没有建立链接则返回null
《8》void getLocalAddress();该方法返回DatagramSocket所链接的本地主机的地址,如果没有建立链接则返回null
《9》void getLocalPort();返回建立了链接的本地UDP端口
《10》void getLocalSocketAddress:该方法返回DatagramSocket所链接的本地主机的地址和端口号,如果没有建立链接则返回null
《11》void getPort();返回建立了链接的远端UDP端口,如果没有建立链接则返回-1
《12》void getReceiveBufferSize();获取此DatagramSocket的SO_RCVBUF选项的值,该值是平台在DatagramSocket上输入时使用的缓冲区大小
《13》void getRemoteSocketAddress();该方法返回DatagramSocket所链接的远程主机的地址和端口号,如果没有建立链接则返回null
《14》void getSendBufferSize();获得发送缓冲区大小
《15》void getSoTimeOut();重新恢复 SO_TIMEOUT 的设置。返回 0 意味着禁用了选项(即无穷大的超时值)。
《16》void setBroadcast(Boolean on);设置是否启动广播机制,true为启动,false为关闭该功能。
《17》setReceiveBufferSize(int size):设置接收缓冲区大小
(4)实现数据报通信
范例:服务器程序的主机地址为本机地址,服务端口号为8080,程序启动后等待接收数据而阻塞,直到接收到UDP数据报才终止阻塞状态。接收到数据后,显示在控制台上,并把收到的数据再发送回客户端。
DatagramServer程序:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class DatagramServer {
private DatagramSocket socket;
public DatagramServer() throws IOException{
//创建DatagramSocket对象,服务端口为8080,地址默认为本机地址
socket=new DatagramSocket(8080);
System.out.println("服务器启动......");
}
public void startServer(){
while(true){
try{
//创建缓冲区大小为1024字节的数据报对象,一次向该缓冲区中放入1024个字节数据
DatagramPacket packet=new DatagramPacket(new byte[1024],1024);
System.out.println("等待接收数据");
//等待接收数据,如果没有接收到数据,进程阻塞
socket.receive(packet);
//把字节数组转换成字符串,其中packet.getData()返回bytes数组型数据
String msg=new String(packet.getData(),0,packet.getLength());
System.out.println("From"+packet.getAddress()+":"+msg);
//通过getBytes()方法把字符串转换成byte数组型,把发送的数据放入packet,这里重新设置了数据报对象packet的缓冲区,
//向数据报缓冲区中写入字节数据
packet.setData(("from server:"+msg).getBytes());
//发送数据报
socket.send(packet);
}catch(IOException ex){
ex.printStackTrace();
}
}
}
public static void main(String[]args) throws IOException{
new DatagramServer().startServer();
}
}
DatagramClient程序:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DatagramClient {
private DatagramSocket socket;
public DatagramClient() throws IOException{
//创建DatagramSocket对象
socket=new DatagramSocket();
}
public void startClient() throws IOException{
try{
//创建服务器的地址
InetAddress remoteIP=InetAddress.getByName("localhost");
//读取用户的标准输入信息,并存入缓冲区对象localReader
BufferedReader localReader=new BufferedReader(new InputStreamReader(System.in));
String msg=null;
while((msg=localReader.readLine())!=null){
//将读到的字符串转化成字节数组
byte[]outputData=msg.getBytes();
//创建要发送的数据报对象,该对象包含要发送的数据,数据长度,远端服务器地址和服务端口
DatagramPacket packet=new DatagramPacket(outputData,outputData.length,remoteIP,8080);
//发出数据报
socket.send(packet);
System.out.println("发出了数据报");
//创建缓冲区大小为1024字节,且一次读取1024个字节的数据报对象
DatagramPacket inputPacket=new DatagramPacket(new byte[1024],1024);
//将收到的数据放入数据报对象inputPacket中
socket.receive(inputPacket);
//打印客户端收到的数据
System.out.println(new String(inputPacket.getData()));
//如果收到的用户输入字符串“end”,则跳出try区块,执行finally区块代码,关闭Socket
if(msg.equals("end"))
break;
}
}catch(IOException ex){
ex.printStackTrace();
}finally{
socket.close();
}
}
public static void main(String[]args) throws IOException{
new DatagramClient().startClient();
}
}