Java网络编程
网络基础
一般情况下,在进行网络编程之前程序员应该掌握与网络有关的知识,甚至对细节也应非常熟悉。由于篇幅所限,只介绍必备的网络基础知识,详细内容请参看相关书籍。
TCP/IP
网络通信协议是计算机间进行通信所遵守的各种规则的集合。Internet的主要协议有:网络层的IP协议;传输层的TCP和UDP协议;应用层的FTP、HTTP、SMTP等协议。其中传输控制协议(Transport Control Protocol,TCP)和网际互联协议(Internet Protocol,IP)是Internet的主要协议,它们定义了计算机与外设进行通信所使用的规则。TCP/IP网络参考模型包括四个层次:应用层、传输层、网络层、链路层。每一层负责不同的功能,下面分别进行介绍:
-
链路层:链路层也称为数据链路层或网络接口层。通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)有关的物理接口细节。
-
网络层:网络层对TCP/IP网络中的硬件资源进行标识。连接到TCP/IP网络中的每台计算机(或其他设备)都有唯一的地址,这就是IP地址。IP地址实际上是一个32位二进制数,通常以"x.x.x.x"的形式表示,其中每个x都是一个0~255的十进制整数。
-
传输层:在TCP/IP网络中,不同的机器之间进行通信时,数据的传输是由传输层控制的,这包括数据要发往的目的主机及应用程序、数据的质量控制等。TCP/IP网络中最常用的传输协议——传输控制协议TCP和用户数据报协议UDP就属于这一层。传输层通常以TCP或UDP来控制端点到端点的通信。用于通信的端点是由Socket来定义的,而Socket是由IP地址和端口号组成的。
TCP是通过在端点与端点之间建立持续的连接而进行通信的。建立连接后,发送端对要发送的数据标记序列号和错误检测代码,并以字节流的方式发送出去;接收端则对数据进行错误检查并按序列顺序将数据整理好,在需要时可以要求发送端重新发送数据,因此,整个字节流到达接收端时完好无缺。这与两个人打电话进行通信的情形类似。
TCP具有可靠性和有序性等特性,并且以字节流的方式发送数据,通常被称为流通信协议。与TCP不同,UDP是一种无连接的传输协议。利用UDP进行数据传输时,首先需要将要传输的数据定义成数据报(datagram),在数据报中指明数据所要到达的Socket(主机地址和端口号),然后再将数据报发送出去。这种传输方式是无序的,也不能确保绝对安全可靠,但它非常简单,也具有比较高的效率,这与通过邮局投递信件进行通信的情形非常相似。
TCP和UDP各有各的用处。当对所传输的数据有时序性和可靠性等要求时,应使用TCP;当传输的数据比较简单、对时序等无要求时,UDP能发挥更好的作用。
-
应用层:大多数基于Internet的应用程序都被看作TCP/IP网络的最上层协议——应用层协议。例如,FTP、HTTP、SMTP、POP3、Telnet等协议。
通信端口
一台机器只能通过一条链路连接到网络上,但一台机器中往往有很多应用程序需要进行网络通信。网络端口号(port)就是用来区分一台主机中的不同应用程序的。
端口号不是物理实体,而是一个标记计算机逻辑通信信道的正整数。端口号是用一个16位的二进制数来表示的,用十进制数来表示的话,其范围为065535,其中,01023被系统保留,专门用于那些通用的服务(well-known service),所以这类端口又被称为熟知端口。例如,HTTP服务的端口号为80,Telnet服务的端口号为21,FTP服务的端口号为23等等。因此,当用户编写通信程序时,应选择一个大于1023的数作为端口号,以免发生冲突。IP使用IP地址把数据投递到正确的计算机上,TCP和UDP使用端口号将数据投递给正确的应用程序。IP地址和端口号组成了所谓的Socket。Socket是网络上运行的程序之间双向通信链路的最后终结点,是TCP和UDP的基础。
URL的概念
URL是统一资源定位器(Uniform Resource Locator)的英文缩写,它表示Internet上某一资源的地址。Internet上的资源包括HTML文件、图像文件、音频文件、视频文件以及其他任何内容(并不完全是文件,也可以是对数据库的一个查询等)。只要按URL规则定义某个资源,那么网络上其他程序就可以通过URL来访问它。也就是说,通过URL访问Internet时,浏览器或其他程序通过解析给定的URL就可以在网络上查找到相应的文件或资源。实际上,用户上网时在浏览器的地址栏中输入的网络就是一个URL。
URL的基本结构由五部分组成,其格式如下:
传输协议://主机名:端口号/文件名#引用
-
传输协议(protocol):指所使用的协议名,如HTTP、FTP等。
-
主机名(hostname):指资源所在的计算机。可以是IP地址,也可以是计算机的名称或域名。
-
端口号(portnumber):一个计算机中可能有多种服务,如Web服务、FTP服务或自己建立的服务等。为了区分这些服务,就需要使用这些端口号,每一种服务用一个端口号。
-
文件名(filename):包括该文件的完整路径。在HTTP中,有一个默认的文件名是index.html,因此,下列两个地址是等价的。
http://java.sun.com
http://java.sun.com/index.html
-
引用(reference):就是资源内部的某个参考点,如http://java.sun.com/index.html#chapter1。
说明:对于一个URL,并不要求它必须包含所有的这五部分内容。
Java语言的网络编程
Java语言的网络编程分为三个层次。
最高一级的网络通信就是从网络上下载小程序。客户端浏览器通过HTML文件中的<applet>标记来识别小程序,并解析小程序的属性,通过网络获取小程序的字节码文件。
次一级的通信就是前面介绍的通过URL类的对象指明文件所在位置,并从网络上下载图像、音频和视频文件等,然后显示图像、对音频和视频进行播放。
最低一级的通信是利用java.net包中提供的类直接在程序中实现网络通信。
针对不同层次的网络通信,Java语言提供的网络功能有四大类:URL、InetAddress、Socket、Datagram。
- URL:面向应用层,通过URL,Java程序可以直接输出或读取网络上的数据。
- InetAddress:面向的是IP层,用于标识网络上的硬件资源。
- Socket和Datagram:面向的是传输层。Socket使用TCP,这是传统网络程序最常用的方式,可以想象为两个不同的程序通过网络的通信信道进行传输(类似于管道运输);Datagram则使用UDP,是另一种网络传输方式,它把数据的目的地址记录在数据包中,然后直接放在网络上(类似于发快递)。
Java语言网络编程中主要使用的java.net包中的类如下:
面向IP层的类:InetAddress;
面向应用层的类:URL、URLConnection;
TCP相关类:Socket、ServerSocket;
UDP相关类:DatagramPacket、DatagramSocket、MulticastSocket。
在使用java.net包中的这些类时,可能产生的异常包括BindException、ConnectException、MalformedURLException、NoRouteToHostException、ProtocolException、SocketException、UnknownHostException、UnknownServiceException。
URL编程
Java语言的URL类和URLConnection类使编程人员能很方便地利用URL在Internet上进行网络通信。
URL类定义了WWW的一个统一资源定位器和可以进行的一些操作。由URL类生成的对象指向WWW资源(如Web页、文本文件、图形图像文件、音频、视频文件等)。
创建URL对象
Java语言利用URL类来访问网络上的资源,URL类是java.lang.Object类的直接子类。下面给出构造方法:
创建URL对象的构造方法 | 功能说明 |
---|---|
public URL(String spec) | 使用URL形式的字符串spec创建一个URL对象 |
public URL(String protocol,String host,int port,String file) | 创建一个协议为protocol、主机名为host、端口号为port、待访问的文件名为file的URL对象 |
public URL(String protocol,String host,String file) | 创建一个协议为protocol、主机名为host、待访问的文件名为file、使用默认端口号的URL对象 |
public URL(String protocol,String host,int port,String file,URLStreamHandler handler) | 创建一个协议为protocol、主机名为host、端口号为port、待访问的文件名为file、URL流句柄为handler的URL对象 |
public URL(URL context,String spec) | 使用已有的URL对象context和URL形式的字符串spec创建URL对象 |
public URL(URL context,String spec,URLStreamHandler handler) | 使用已有的URL对象context和URL形式的字符串spec创建URL流句柄为handlerURL的对象 |
在创建URL对象时,若发生错误,系统会产生MalformedURLException异常,这是非运行时异常,必须在程序中捕获处理。例如:
URL url1,url2,url3;
try {
url1=new URL("file:/D:/image/test.gif");
url2=new URL("http://www.sohu.com/map/");
url3=new URL(url2,"test.gif");
} catch (MalformedURLException e) {
e.printStackTrace();
}
除了最基本的构造方法之外,URL类中还有一些简单实用的方法,利用这些方法可以得到URL位置本身的数据,或是将URL对象转换成表示URL位置的字符串。如下表:
URL类的常用方法 | 功能说明 |
---|---|
public boolean equals(Object obj) | 判断两个URL是否相同 |
public final Object getContent() | 获取URL连接的内容 |
public String getProtocol() | 返回URL对象的协议名称 |
public String getHost() | 返回URL对象访问的计算机名称 |
public int getPort() | 返回URL对象访问的端口号 |
public String getFile() | 返回URL对象指向的文件名 |
public String getPath() | 返回URL对象所使用的文件路径 |
public String getRef() | 返回URL对象的引用字符串,即获取参考点 |
public URLConnection openConnection() | 打开URL指向的连接 |
public final InputStream openStream() | 打开输入流 |
protected void set(String protocol,String host,int port,String file,String ref) | 用给定参数设置URL中各字段的内容 |
public String toString() | 返回整个URL字符串 |
使用URL类访问网络资源
案例:通过URL类直接读取网络上服务器中的文本内容。利用URL访问http://www.edu.cn/index.html文件,即访问教育网上的index.html文件。读取网络上文件内容一般分为三个步骤:一是创建URL类的对象;二是利用URL类的openStream()方法获得对应的InputStream类的对象;三是通过InputStream对象来读取文件内容。
public class Test1 {
public static void main(String[] args) {
String urlName="http://www.edu.cn/index.html";
new Test1().display(urlName);
}
public void display(String urlName){
try {
URL url=new URL(urlName);
InputStreamReader in=new InputStreamReader(url.openStream());
BufferedReader br=new BufferedReader(in);
String aLine;
while ((aLine=br.readLine())!=null){
System.out.println(aLine);
}
} catch (MalformedURLException e) {
System.out.println(e);
} catch (IOException e) {
System.out.println(e);
}
}
}
输出结果:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "//www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="//www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta content="IE=EmulateIE7" http-equiv="X-UA-Compatible">
<title>中国教育和科研计算机网CERNET</title>
……略
用Java语言实现底层网络通信
用Java语言实现计算机网络的底层通信,就是在用Java程序实现网络通信协议所规定的功能,这是Java语言网络编程的一部分。
InetAddress程序设计
众所周知,Internet上主机的地址有两种表示方式,即域名和IP地址。java.net包中的InetAddress类的对象包含一个Internet主机的域名和IP地址,如www.sina.com.cn和202.108.35.210(域名和IP地址)。因此,在已知一个InetAddress对象时,就可以通过一定的方法从中获取Internet上主机的地址(域名或IP地址)。由于每个InetAddress对象中包括了IP地址、主机名(域名)等信息,所以使用InetAddress类可以在程序中用主机名代替IP地址,从而使程序更加灵活,可读性更好,例如用www.sina.com.cn代替202.108.35.210。
InetAddress类没有构造方法,因此不能用new运算符来创建InetAddress对象,通常是用它提供的静态方法来获取。下面给出InetAddress类的常用方法:
InetAddress类的常用方法 | 功能说明 |
---|---|
public static InetAddress getByName(String host) | 通过给定的主机名host,获取InetAddress对象的IP地址 |
public static InetAddress getByAddress(byte[] addr) | 通过存放在字节数组中的IP地址,返回一个InetAddress对象 |
public static InetAddress getLocalHost() | 获取本地主机的IP地址 |
public byte[] getAddress() | 获取本对象的IP地址,并存放在字节数组中 |
public String getHostAddress() | 利用InetAddress对象,获取该对象的IP地址 |
public String getHostName() | 利用InetAddress对象,获取该对象的主机名 |
public String toString() | 将IP地址转换成字符串形式的域名 |
该表中给的static方法通常会产生UnknownHostException异常,应在程序中捕获处理。
案例:编写一个Java应用程序,直接查询自己主机的IP地址和Internet上WWW服务器的IP地址。
public class Test1 {
public static void main(String[] args) {
InetAddress myIPAddress=null;
InetAddress myServer=null;
try {
myIPAddress=InetAddress.getLocalHost();
} catch (UnknownHostException e) { }
try {
myServer=InetAddress.getByName("www.tom.com");
} catch (UnknownHostException e) { }
System.out.println("您主机的IP地址为:"+myIPAddress);
System.out.println("服务器的IP地址为:"+myServer);
}
}
输出结果:
您主机的IP地址为:LAPTOP-I8KK4HDT/192.168.137.1
服务器的IP地址为:www.tom.com/218.98.50.24
基于连接的Socket通信程序设计
Socket通信属于网络底层通信,它是网络上运行的两个程序间双向通信的一端,它既可以接收请求,,也可以发送请求,利用它可以较方便地进行网络上的数据传输。Socket是实现客户与服务器(Client/Server,C/S)模式的通信方式,它首先需要建立稳定的连接,然后以流的方式传输数据,实现网络通信。Socket原意为“插座”,在通信领域中译为“套接字”,意思是将两个物品套在一起,在网络通信里的含义就是建立一个连接。
1.Socket通信机制的基本概念
- 建立连接
当两台计算机进行通信时,首先要在两者之间建立一个连接,也就是两者分别运行不同的程序,由一端发出连接请求,另一端等候连接请求。当等候端收到请求并接受请求后,两个程序就建立起一个连接,之后通过这个连接就可以进行数据交换。此时,请求方称为“客户端”,接收方称为“服务器”,这是计算机通信的一个基本机制,称为客户、服务器模式。打个比方来说,这个机制和电话系统是类似的,即必须有一方拨打电话,而另一方等候铃响并决定是否接听,当接听后就可以进行电话交流,呼叫的一方称为客户,负责监听的一方称为服务器。应用在这两端的TCP Socket分别称为客户Socket和服务器Socket。
- 连接地址
为了建立连接,需要由一个程序向另一台计算机上的程序发出请求,其中,能够唯一识别对方机器的就是计算机的名称或IP地址。在Internet中,能唯一标识计算机IP地址的是连接地址,IP地址类似于电话系统中的电话号码。
仅仅有连接地址是不够的,还必须有端口号。因为一台机器上可能会启动很多程序,必须为每个程序分配一个唯一的端口号,通过端口号来指定要连接的那个程序。因此,一个完整的连接地址应该是计算机的IP地址加上连接程序的端口号。
在两个程序进行连接之前要约定好端口号。由服务器端分配端口号并等候请求,客户端利用这个端口号发出连接请求,当两个程序所设定的端口号一致时连接建立成功。
- TCP/IP Socket通信
Socket在TCP/IP中定义,用以针对一个特定的连接。每台机器上都有一个套接字,可以想象它们之间有一条虚拟的线缆,线缆的每一端都插入一个套接字或插座里。在Java语言中,服务器端套接字使用的是ServerSocket类对象,客户端套接字使用的是Socket类对象,由此区分服务器端和客户端。
2.Socket类与ServerSocket类
- Socket类
Socket类在java.net包中,java.net.Socket继承自java.lang.Object类。Socket类用在客户端,用户通过创建一个Socket对象来建立与服务器的连接。Socket连接可以是流连接,也可以是数据报连接,这取决于创建Socket对象时所使用的构造方法。要求可靠性高的通信一般采用流连接。流连接的优点是所有数据都能准确有序地发送到对方,缺点是速度较慢。流式Socket所完成的通信是基于连接的通信,即在通信开始之前先由通信双方确认身份并建立一条专用的虚拟连接通信,然后它们通过这条通道传送数据信息进行通信,当通信结束时再将原先所建立的连接拆除。下面给出构造方法和常用方法:
Socket类的构造方法 | 功能说明 |
---|---|
public Socket(String host,int port) | 在客户端以指定的服务器地址host和端口号port,创建一个Socket对象,并向服务器端发出连接请求 |
public Socket(InetAddress address,int port) | 同上,但IP地址由address指定 |
public Socket(String host,int port,boolean stream) | 同上,但若stream为真,则创建流Socket对象,否则创建数据报Socket对象 |
public Socket(InetAddress host,int port,boolean stream) | 同上,但IP地址由host指定 |
Socket类的常用方法 | 功能说明 |
---|---|
public InetAddress getInetAddress() | 获取创建Socket对象时指定的服务器的IP地址 |
public InetAddress getLocalAddress() | 获取创建Socket对象时客户计算机的IP地址 |
public InputStream getInputStream() | 为当前的Socket对象创建输入流 |
public OutputStream getOutputStream() | 为当前的Socket对象创建输出流 |
public int getPort() | 获取创建Socket对象时指定远程主机的端口号 |
public void setReceiveBufferSize(int size) | 设置接收缓冲区的大小 |
public int getReceiveBufferSize() | 返回接收缓冲区的大小 |
public void setSendBufferSize() | 设置发送缓冲区的大小 |
public int getSendBufferSize() | 返回发送缓冲区的大小 |
public void close() | 关闭建立的Socket连接 |
- ServerSocket类
在Socket编程中,服务器端使用ServerSocket类。ServerSocket类在java.net包中,java.net.ServerSocket继承自java.lang.Object类。ServerSocket类的作用是实现客户机/服务器模式的通信方式下服务器端的套接字。下面给出构造方法和常用方法:
ServerSocket类的构造方法 | 功能说明 |
---|---|
public ServerSocket(int port) | 以指定的端口port创建ServerSocket对象并等候客户端的连接请求。端口号必须与客户端呼叫用的端口号相同 |
public ServerSocket(int port,int backlog) | 同上,但以backlog指定最大的连接数——可同时连接的客户端数量 |
ServerSocket类的常用方法 | 功能说明 |
---|---|
public Socket accept() | 在服务器端的指定端口监听客户端发来的连接请求,并返回一个与客户端Socket对象相连接的Socket对象 |
public InetAddress getInetAddress() | 返回服务器的IP地址 |
public int getLocalPort() | 返回服务器的端口号 |
public void close() | 关闭服务器端建立的套接字 |
3.Socket通信模式
前面已经介绍过,当两个程序进行通信时,可以通过使用Socket类建立套接字连接。
- 客户建立到服务器的套接字对象
客户端的程序使用Socket类建立与服务器的套接字连接。由于在使用Socket构造方法创建Socket对象时会产生IOException异常,所以在创建Socket对象时应处理IOException。例如:
try {
Socket mySocket=new Socket("http://www.gduf.edu.cn",1880);
} catch (IOException e) { }
当套接字连接建立后,一条通信线路就建立起来了。mySocket对象可以使用getInputStream()方法获得输入流,然后用这个输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息);同理mySocket对象还可以使用getOutputStream()方法获得输出流,然后用这个输出流将信息写入线路。
在实际编写程序时,把mySocket对象使用getInputStream()方法获得的输入流连接到另一个数据流DataInputStream上,然后就可以从这个数据流中读取来自服务器的信息,这样做的原因是数据流DataInputStream有更好的从流中读取信息的方法。同样,把mySocket对象使用getOutputStream()方法获得的输出流连接到DataOutputStream流上,然后向这个数据流写入信息,发送给服务器端。
- 建立接收客户套接字的服务器套接字
我们已经知道客户负责建立客户到服务器的套接字连接,即客户负责呼叫。因此,服务器必须建立一个等待接收客户套接字的服务器套接字。服务器端的程序使用ServerSocket类建立接收客户套接字的服务器套接字。同样在使用ServerSocket构造方法创建ServerSocket对象时也会产生IOException异常,所以在创建ServerSocket对象时也应处理IOException异常。例如:
try {
ServerSocket serSocket=new ServerSocket(1880);
} catch (IOException e) { }
当服务器的套接字serSocket建立后,就可以利用accept()方法接收客户的套接字mySocket。接收过程如下:
try {
Socket sc=serSocket.accept();
} catch (IOException e) { }
将收到的客户端的mySocket放到一个已声明的Socket对象sc中。这样服务器端的sc就可以使用getOutputStream()方法获得输出流,然后用这个输出流向线路写信息,发送到客户端。同样,可以使用getInputStream()方法获得一个输入流,用这个输入流读取客户放入线路的信息。
综上,Socket通信的步骤如下:
- 在服务器端创建一个ServerSocket对象,并指定端口号;
- 运行ServerSocket的accept()方法,等候客户端请求;
- 客户端创建一个Socket对象,指定服务器的IP地址和端口号,向服务器端发出连接请求;
- 服务器端接收到客户端请求后,创建Socket对象与客户端建立连接;
- 服务器端和客户端分别建立输入输出数据流,进行数据传输;
- 通信结束后,服务器端和客户端分别关闭相应的Socket连接;
- 服务器端程序运行结束后,调用ServerSocket对象的close()方法停止等候客户端请求;
由此可以看出对于一个网络通信程序来说,需要编写服务器端和客户端两个程序才能实现相互通信。为了能实现服务器端同时对多个客户进行服务,需要用多线程,在服务器端创建客户请求的监听线程,一旦客户发起连接请求,则在服务器端创建用于服务的Socket,利用该Socket完成与客户的通信,即每个线程针对一个客户进行服务。数据传输结束后,终止运行该Socket通信的线程,继续在服务器端指定的端口进行监听。
案例:编写一个点对点的聊天程序,服务器端程序在8080端口创建一个ServerSocket对象,然后调用该对象的accept()方法等待客户端的请求;当有客户端发来请求时,则创建一个Socket对象与客户端建立连接;之后用Socket对象创建输入流对象sin和输出流对象sout,然后用约定的格式进行数据传输;数据传输时启动线程,使用输入流对象sin按行显示从客户端发来的字符串,用输出流对象sout将从键盘输入的一行字符串发送到客户端;当客户端或服务器端发出"bye"字符串时,表示数据传输结束,关闭相应的流和Socket连接,等待下一次连接。
服务器端的程序代码如下:
public class MyServer implements Runnable{
ServerSocket server=null;
Socket clientSocket;//负责当前线程中C/S通信中的Socket对象
boolean flag=true;//标记是否结束
Thread connenThread;//向客户端发送信息的线程
BufferedReader sin;
DataOutputStream sout;
public static void main(String[] args) {
new MyServer().serverStart();
}
public void serverStart(){
try {
server=new ServerSocket(8080);//建立监听服务
System.out.println("端口号:"+server.getLocalPort());
while (flag){
clientSocket= server.accept();
System.out.println("连接已经建立完毕!");
InputStream is=clientSocket.getInputStream();
sin=new BufferedReader(new InputStreamReader(is));
OutputStream os=clientSocket.getOutputStream();
sout=new DataOutputStream(os);
connenThread=new Thread(this);
connenThread.start();//启动线程,向客户端发送信息
//从客户端读入信息
String aLine;
while ((aLine=sin.readLine())!=null){
System.out.println(aLine);
if (aLine.equals("bye")){
flag=false;
connenThread.interrupt();
break;
}
}
sout.close();//关闭流
os.close();
sin.close();
is.close();
clientSocket.close();//关闭Socket连接
System.exit(0);
}
} catch (Exception e) { System.out.println(e); }
}
@Override
public void run() {
while (true){
try {
int ch;//通过键盘接收字符并向客户端发送
while (((ch=System.in.read())!=-1)){
sout.write((byte)ch);
if (ch=='\n') sout.flush();//将缓冲区内容向客户端输出
}
} catch (Exception e) { System.out.println(e); }
}
}
}
当客户连接到指定的8080端口时,服务器端就建立线程来专门处理与这个客户间的通信,即向客户端写入一系列的字符串信息,并从客户端读取一段信息显示在服务器端。所以该程序中有两个线程:一个是接收客户端的数据;另一个是向客户端发送数据。由于这是两个并发的线程,所以当连接建立后立即启动线程和客户端,
public class MyClient implements Runnable {
Socket clientSocket;
boolean flag;
Thread connenThread;
BufferedReader cin;
DataOutputStream cout;
public static void main(String[] args) {
new MyClient().clientStart();
}
public void clientStart() {
try {
clientSocket = new Socket("localhost", 8080);
System.out.println("已建立连接!");
while (flag) {
InputStream is = clientSocket.getInputStream();
cin = new BufferedReader(new InputStreamReader(is));
OutputStream os = clientSocket.getOutputStream();
cout = new DataOutputStream(os);
connenThread = new Thread(this);
connenThread.start();//启动线程,向服务器端发送信息
//接收服务器端的数据
String aLine;
while ((aLine = cin.readLine()) != null) {
System.out.println(aLine);
if (aLine.equals("bye")) {
flag = false;
connenThread.interrupt();
break;
}
}
cout.close();//关闭流
os.close();
cin.close();
is.close();
clientSocket.close();//关闭Socket连接
System.exit(0);
}
} catch (Exception e) {
System.out.println(e);
}
}
@Override
public void run() {
while (true) {
try {
int ch;//通过键盘接收字符并向服务器端发送
while (((ch = System.in.read()) != -1)) {
cout.write((byte) ch);
if (ch == '\n') cout.flush();//将缓冲区内容向输出流发送
}
} catch (Exception e) {
System.out.println(e);
}
}
}
}//补充:有bug
在客户端首先要指定服务器端地址(本机)和8080端口号,创建一个Socket对象,向服务器端发出连接请求,当服务器端获得请求并建立连接后,即可进行数据通信;当连接建立完后,使用Socket对象创建输入流对象cin和输出流对象cout,然后按约定的格式进行数据传输;在进行通信时首先要启动线程,使用输出流对象cout将从键盘输入的一行字符串向服务器端发送,同时使用输入流对象cin,按行显示从服务器端发送来的字符串;当服务器端或客户端发出"bye"字符串时表示数据传输结束,关闭相应的流和Socket连接,程序运行结束。
注意:如果要在同一台计算机上运行服务器端和客户端两个程序,需要启动两个DOS窗口,分别模拟服务器端和客户端,但要首先运行服务器端程序。如果是在两台计算机上分别运行服务器端程序和客户端程序,则应修改客户端程序的服务器地址。
说明:客户端的操作与服务器端的操作基本相同,区别在于建立连接的方式不同。服务器端创建ServerSocket对象,并调用accept()方法等候客户端的请求,而客户端是创建Socket对象发送请求。
无连接的数据报通信程序设计
流式Socket是基于TCP的网络套接字技术,这种通信方式可以实现准确的通信,但是占用资源较多,在某些无需实时交互的情况下,例如收发E-mail等,采用保持连接的流式通信并不恰当,而应该使用无连接的数据报方式。
数据报通信是基于用户数据报协议(User Datagram Protocol,UDP)的网络信息传输方式。数据报(datagram)是网络层数据单元在介质上传输信息的一种逻辑分组形式。数据报是无连接的远程通信服务,它是一种在网络中传输的、独立的、自身包含地址信息的数据单位,不保证传送顺序和内容的准确性。数据报Socket又称为UDP套接字,它无需建立、拆除连接,而是直接将信息打包传向指定的目的地,使用起来比流式Socket简单一些。但由于这种通信方式不能保证将所有数据都传送到目的地,所以一般用于传送非关键性的数据。
数据报通信的基本模式是,首先将数据打包,形成数据包,类似于将信件装入信封,然后将数据包发往目的地;其次是接收端收到别人发来的数据包,然后查看数据包中的内容,类似于从信封中取出信件。
Java语言中用于无连接的数据报通信使用Java类库中java.net包中的两个类DatagramPacket和DatagramSocket。其中,DatagramPacket类在发送端用于将待发送的数据打包,在接收端则用于将收到的数据拆包;DatagramSocket类用于实现数据报通信的过程中数据报的发送与接收。
1.DatagramPacket类
利用数据报通信时,发送端使用DatagramPacket类将数据打包,即用DatagramPacket类创建一个数据报对象,它包含有需要传输的数据、数据报的长度、IP地址和端口号等信息。接收端则利用DatagramPacket类对象将接收到的数据拆包,该对象一般只包含要接收的数据和该数据长度两个参数。下面给出构造方法和常用方法:
DatagramPacket类的构造方法 | 功能说明 |
---|---|
public DatagramPacket(byte[] buf,int length) | 创建一个用于接收数据报的对象,buf数组用于接收数据报中的数据,接收长度为length |
public DatagramPacket(byte[] buf,int length,InetAddress address,int port) | 创建一个用于发送给远程系统的数据报对象。并将数组buf中长度为length的数据发送到地址为address、端口号为port的主机上 |
DatagramPacket类的常用方法 | 功能说明 |
---|---|
public byte[] getData() | 返回一个字节数组,包含收到或要发送的数据报中的数据 |
public int getLength() | 返回发送或接收到的数据的长度 |
public InetAddress getAddress() | 返回目标数据包的IP地址或发送该数据包的主机的IP地址 |
public int getPort() | 返回目标数据包的端口号或发送该数据包的主机的端口号 |
2.DatagramSocket类
DatagramSocket类用于在发送主机中建立数据报通信方式,提出发送请求,实现数据报的发送与接收。下面给出构造方法和常用方法:
DatagramSocket类的构造方法 | 功能说明 |
---|---|
public DatagranSocket() | 创建一个以当前计算机的任何一个可用端口为发送端口的数据报连接 |
public DatagranSocket(int port) | 创建一个以当前计算机的指定端口port为接收端口的数据报连接 |
public DatagranSocket(int port,InetAddress laddr) | 用于在有多个IP地址的当前主机上,创建一个以laddr为指定IP地址、以port为指定端口的数据报连接 |
这三个构造方法都可能抛出SocketException异常,所以要用try-catch块来控制在创建DatagramSocket对象时可能产生的异常情况。
DatagramSocket类的常用方法 | 功能说明 |
---|---|
public void receive(DatagramPacket p) | 从建立的数据报连接中接收数据,并保存到p中 |
public void send(DatagramPacket p) | 将数据报对象p中包含的报文发送到所指定的IP地址主机的指定端口 |
public void setSoTimeout(int timeout) | 设置传输超时为timeout |
public void close() | 关闭数据报连接 |
说明:由于数据报是不可靠的通信方式,所以receive()方法不一定能接收到数据,为防止线程死亡,应该利用setSoTimeout()方法设置超时参数timeout。另外,receive()和send()方法都可能产生输入、输出异常,所以都可能抛出IOException异常。
3.数据报通信的发送与接收过程
数据报发送过程的步骤如下:
-
创建一个用于发送数据的DatagramPacket对象,使其包含如下信息:
- 要发送的数据;
- 数据报分组的长度;
- 发送目的地的主机的IP地址和目的端口号。
-
在指定的或可用的本机端口创建DatagramSocket对象。
-
调用DatagramSocket对象的send()方法,以DatagramPacket对象为参数发送数据报。
数据报接收过程的步骤如下:
- 创建一个用于接收数据报的DatagramPacket对象,其中包含空白数据缓冲区和指定数据包分组的长度。
- 在指定的或可用的本机端口创建DatagramSocket对象。
- 调用DatagramSocket对象的receive()方法,以DatagramPacket对象为参数接收数据报,接收到的信息有:
- 收到的数据报分组的内容;
- 发送端的主机的IP地址;
- 发送端的主机的发送端口号。
案例:编写一个数据报通信程序,客户端向服务器端发送信息,服务器端将收到的信息显示在窗口中。
客户端程序代码:
public class UDPClient {
public static void main(String[] args) {
UDPClient frm=new UDPClient();
}
CliThread ct;//声明客户类线程对象ct
public UDPClient(){//构造方法
ct=new CliThread();//创建线程
ct.start();//启动线程
}
}
class CliThread extends Thread{//客户端线程类,负责发送信息
@Override
public void run() {
String str1;
String serverName="LAPTOP-I8KK4HDT";//服务器端计算机名
System.out.println("请发送信息给服务器《"+serverName+"》");
try {
DatagramSocket skt=new DatagramSocket();//建立UDPSocket对象
DatagramPacket pkt;//建立DatagramPacket对象pkt
while(true){
BufferedReader buf;
buf=new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入信息:");
str1= buf.readLine();//从键盘上读取数据
byte[] outBuf=new byte[str1.length()];
outBuf=str1.getBytes();
//下面取得服务器端地址
InetAddress address=InetAddress.getByName(serverName);
pkt=new DatagramPacket(outBuf,outBuf.length,address,8000);//数据打包
skt.send(pkt);
}
} catch (Exception e) {
System.out.println(e);
}
}
}
说明:客户端程序中的serverName必须用所使用的计算机名来作为服务器端的计算机名,否则程序将无法运行。serverName可由如下语句获取并输出:
System.out.println(InetAddress.getLocalHost().getHostName());
服务器端程序代码:
public class UDPServer {
public static void main(String[] args) {
UDPServer frm=new UDPServer();
}
String strbuf=" ";
SerThread st;//声明服务器类线程对象st
public UDPServer(){
st=new SerThread();//创建线程
st.start();//启动线程
}
}
class SerThread extends Thread{//服务器类线程,负责接收信息
@Override
public void run() {
String str1;
try {//使用8000端口,建立UDPSocket对象
DatagramSocket skt=new DatagramSocket(8000);
System.out.print("服务器名:");
//显示服务器计算机的名称
System.out.println(InetAddress.getLocalHost().getHostName());
while (true){
byte[] inBuf=new byte[256];
DatagramPacket pkt;
//创建并设置接收pkt对象的接收信息
pkt=new DatagramPacket(inBuf,inBuf.length);
skt.receive(pkt);//接收数据报分组
//提取接收到的分组中的数据并转成字符串
str1=new String(pkt.getData());
str1=str1.trim();//去掉字符串中的首尾空格
if (str1.length()>0){
int pot=pkt.getPort();//获取远程端口号
System.out.println("远程端口:"+pot);
System.out.println("服务器已接收到信息:"+str1);
}
}
} catch (Exception e) {
System.out.println(e);
}
}
}
在数据包通信中,由于通信双方之间并不需要建立连接,所以服务器端应用程序通信过程与客户端应用程序的通信过程是非常相似的,客户端与服务器端双方均可以发送与接收数据报分组。所不同的是服务器应用程序要面向网络中所有的计算机,所以服务器应用程序收到一个数据报分组后要分析它,得到数据报的源地址信息,这样才能创建正确的返回结果分组给客户机。
本章小结
- 通信端口是一个标记计算机逻辑通信信道的正整数,用于区分一台主机中的不同应用程序,端口号不是物理实体。
- IP地址和端口号组成了所谓的Socket。Socket是实现客户与服务器(Client/Server,C/S)模式的通信方式,Socket原意为“插座”,在通信领域中译为“套接字”,在网络通信里的含义就是建立一个连接。
- URL是统一资源定位器(Uniform Resource Locator)的简称,它表示Internet上某一资源的地址。URL的基本结构由五部分组成。
- Java的网络编程分为三个层次。最高一级的网络通信就是从网络上下载小程序;次一级的通信就是通过URL类的对象指明文件所在位置,并从网络上下载音频、视频或图像文件,然后播放音频、视频或显示图像;最低一级的通信是利用java.net包中提供的类直接在程序中实现网络通信。
- 针对不同层次的网络通信,Java语言提供的网络功能有四大类:URL、InetAddress、Socket、Datagram。
- URL:面向应用层,通过URL,Java程序可以直接输出或读取网络上的数据。
- InetAddress:面向的是IP层,用于标识网络上的硬件资源。
- Socket和Datagram:面向的是传输层。Socket使用TCP,这是传统网络程序最常用的方式,可以想象为两个不同的程序通过网络的通信信道进行通信;Datagram则使用UDP,是另一种网络传输方式,它把数据的目的地址记录在数据包中,然后直接放在网络上。
课后习题
- 编写Java程序,使用InetAddress类实现根据域名自动到DNS(域名服务器)上查找IP地址的功能。