二十一章网络通信
计算机网络实现了多台计算机间的互联,使得它们彼此之间能够进行数据交流。网络应用程序就是在已连接的不同计算机上运行的程序,这些程序借助于网络协议,相互之间可以交换数据。编写网络应用程序前,首先必须明确所要使用的网络协议。TCP/IP协议是网络应用程序的首选
21.1 网络程序设计基础
网络程序设计编写的是与其他计算机进行通信的程序。Java已经将网络程序所需要的元素封装成不同的类,用户只要创建这些类的对象,使用相应的方法,即使不具备有关的网络知识,也可以编写出高质量的网络通信程序。
21.1.1 局域网与互联网
为了实现两台计算机的通信,必须用一个网络线路连接两台计算机
服务器是指提供信息的计算机或程序,客户机是指请求信息的计算机或程序。网络用于连接服务器与客户机,实现两者间的相互通信。但是,有时在某个网络中很难将服务器与客户机区分开。我们通常所说的局域网(Local Area Network,LAN),就是一群通过一定形式连接起来的计算机,它可以由两台计算机组成,也可以由同一区域内的上千台计算机组成。将LAN延伸到更大的范围,这样的网络称为广域网(Wide Area Network,WAN)。我们熟悉的互联网(Internet),就是由无数的LAN和WAN组成的。
21.1.2 网络协议
网络协议规定了计算机之间连接的物理、机械(网线与网卡的连接规定)、电气(有效的电平范围)等特征,计算机之间的相互寻址规则,数据发送冲突的解决方式,长数据如何分段传送与接收等内容。就像不同的国家有不同的法律一样,目前网络协议也有多种。下面简单地介绍几个常用的网络协议。
1.IP协议
IP是Internet Protocol的简称,是一种网络协议。Internet网络采用的协议是TCP/IP协议,其全称是Transmission Control Protocol/Internet Protocol。Internet依靠TCP/IP协议,在全球范围内实现了不同硬件结构、不同操作系统、不同网络系统间的互联。在Internet网络上存在着数以亿计的主机,每台主机都用网络为其分配的Internet地址代表自己,这个地址就是IP地址。到目前为止,IP地址用4个字节,也就是32位的二进制数来表示,称为IPv4。为了便于使用,通常取用每个字节的十进制数,并且每个字节之间用圆点隔开来表示IP地址,如192.168.1.1。现在人们正在试验使用16个字节来表示IP地址,这就是IPv6,但IPv6还没有投入使用。
TCP/IP模式是一种层次结构,共分为4层,分别为应用层、传输层、互联网层和网络层。各层实现特定的功能,提供特定的服务和访问接口,并具有相对的独立性。
2.TCP与UDP协议
在TCP/IP协议栈中,有两个高级协议是网络应用程序编写者应该了解的,即传输控制协议(Transmission Control Protocol,TCP)与用户数据报协议(User Datagram Protocol,UDP)。
TCP协议是一种以固接连线为基础的协议,它提供两台计算机间可靠的数据传送。TCP可以保证数据从一端送至连接的另一端时,能够确实送达,而且抵达的数据的排列顺序和送出时的顺序相同。因此,TCP协议适合可靠性要求比较高的场合。就像拨打电话,必须先拨号给对方,等两端确定连接后,相互才能听到对方说话,也知道对方回应的是什么。
HTTP、FTP和Telnet等都需要使用可靠的通信频道。例如,HTTP从某个URL读取数据时,如果收到的数据顺序与发送时不相同,可能就会出现一个混乱的HTML文件或是一些无效的信息。
UDP是无连接通信协议,不保证数据的可靠传输,但能够向若干个目标发送数据,或接收来自若干个源的数据。UDP以独立发送数据包的方式进行。这种方式就像邮递员送信给收信人,可以寄出很多信给同一个人,且每一封信都是相对独立的,各封信送达的顺序并不重要,收信人接收信件的顺序也不能保证与寄出信件的顺序相同。
UDP协议适合于一些对数据准确性要求不高,但对传输速度和时效性要求非常高的网站,如网络聊天室、在线影片等。这是由于TCP协议在认证上存在额外耗费,可能使传输速度减慢,而UDP协议即使有一小部分数据包遗失或传送顺序有所不同,也不会严重危害该项通信。
注意
一些防火墙和路由器会设置成不允许UDP数据包传输,因此若遇到UDP连接方面的问题,应先确定所在网络是否允许UDP协议。
21.1.3 端口与套接字
一般而言,一台计算机只有单一的连到网络的物理连接(Physical Connection),所有的数据都通过此连接对内、对外送达特定的计算机,这就是端口。网络程序设计中的端口(port)并非真实的物理存在,而是一个假想的连接装置。端口被规定为一个在0~65535的整数。HTTP服务一般使用80端口,FTP服务使用21端口。假如一台计算机提供了HTTP、FTP等多种服务,那么客户机会通过不同的端口来确定连接到服务器的哪项服务上,如图21.3所示。
通常,0~1023的端口数用于一些知名的网络服务和应用,用户的普通网络应用程序应该使用1024以上的端口数,以避免端口号与另一个应用或系统服务所用端口冲突。
网络程序中的套接字(Socket)用于将应用程序与端口连接起来。套接字是一个假想的连接装置,就像插座一样可连接电器与电线,如图21.4所示。Java将套接字抽象化为类,程序设计者只需创建Socket类对象,即可使用套接字。
21.2 TCP程序
TCP网络程序设计是指利用Socket类编写通信程序。利用TCP协议进行通信的两个应用程序是有主次之分的,一个称为服务器程序,另一个称为客户机程序,两者的功能和编写方法大不一样。
①——服务器程序创建一个ServerSocket(服务器端套接字)对象,调用accept()方法等待客户机来连接。
②——客户端程序创建一个Socket对象,请求与服务器建立连接。
③——服务器接收客户机的连接请求,同时创建一个新的Socket对象与客户建立连接。随后服务器继续等待新的请求。
21.2.1 InetAddress类
java.net包中的InetAddress类是与IP地址相关的类,利用该类可以获取IP地址、主机地址等信息。
使用InetAddress类的getHostName()和getHostAddress()方法获得本地主机的本机名、本机IP地址。
package Package21;
import java.net.InetAddress;//导入包
import java.net.UnknownHostException;
public class Address {//创建类
public static void main(String[] args) {//创建InetAddress对象
// TODO Auto-generated method stub
InetAddress ip;
try {//捕捉可能出现的异常
ip = InetAddress.getLocalHost();//实例化对象
String locaIname = ip.getHostName();//获取本机名
String locaIip = ip.getHostAddress();//获取本机IP地址
System.out.println("本机名:"+locaIname);//将本机名输出
System.out.println("本机IP地址:"+locaIip);//将本机IP地址输出
}catch(UnknownHostException e) {
e.printStackTrace();//输出异常信息
}
}
}
结果:
注意
InetAddress类的方法会抛出UnknownHostException异常,所以必须进行异常处理。这个异常在主机不存在或网络连接错误时发生。
21.2.2 ServerSocket类
java.net包中的ServerSocket类用于表示服务器套接字,其主要功能是等待来自网络上的“请求”,它可通过指定的端口来等待连接的套接字。服务器套接字一次可以与一个套接字连接。如果多台客户机同时提出连接请求,服务器套接字会将请求连接的客户机存入列队中,然后从中取出一个套接字,与服务器新建的套接字连接起来。若请求连接数大于最大容纳数,则多出的连接请求被拒绝。队列的默认大小是50。
ServerSocket类的构造方法通常会抛出IOException异常,具体有以下几种形式:
- ServerSocket():创建非绑定服务器套接字。
- ServerSocket(int port):创建绑定到特定端口的服务器套接字。
- ServerSocket(int port, int backlog):利用指定的backlog创建服务器套接字,并将其绑定到指定的本地端口号上。
- ServerSocket(int port, int backlog, InetAddress bindAddress):使用指定的端口、侦听backlog和要绑定到的本地IP地址创建服务器。这种情况适用于计算机上有多块网卡和多个IP地址的情况,用户可以明确规定ServerSocket在哪块网卡或哪个IP地址上等待客户的连接请求。
调用ServerSocket类的accept()方法,会返回一个和客户端Socket对象相连接的Socket对象。服务器端的Socket对象使用getOutputStream()方法获得的输出流,将指向客户端Socket对象使用getInputStream()方法获得的那个输入流;同样,服务器端的Socket对象使用getInputStream()方法获得的输入流,将指向客户端Socket对象使用getOutputStream()方法获得的那个输出流。也就是说,当服务器向输出流写入信息时,客户端通过相应的输入流就能读取,反之亦然。
注意
accept()方法会阻塞线程的继续执行,直到接收到客户的呼叫。如果没有客户呼叫服务器,那么System.out.println("连接中")语句将不会执行。语句如果没有客户请求,accept()方法没有发生阻塞,肯定是程序出现了问题。通常是使用了一个被其他程序占用的端口号,ServerSocket绑定没有成功。
yu = server.accept();
System.out.println("连接中");
21.2.3 TCP网络程序设计
明白了TCP程序工作的过程,就可以编写TCP服务器程序了。在网络编程中,如果只要求客户机向服务器发送消息,不要求服务器向客户机发送消息,称为单向通信。客户机套接字
和服务器套接字连接成功后,客户机通过输出流发送数据,服务器则通过输入流接收数据。下面是简单的单向通信的实例。
【例21.2】创建TCP/IP协议服务器
package Package21;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {//服务器套接字
private ServerSocket server;//客户机套接字
private Socket socket;
void start() {//启动服务器
try {
server = new ServerSocket(8998);//服务器启动8998端口
System.out.println("服务器套接字已经创建成功");
while(true) {
System.out.println("等待客户机的连接");
socket = server.accept();//服务器监听客户机连接
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//根据套接字字节流创建字符输入流
while(true) {//循环接收信息
String message = reader.readLine();//读取一行文字
if("exit".equals(message)) {//入过客户机发来的内容为"exit"
System.out.println("客户机退出");
break;//停止接收信息
}
System.out.println("客户机:"+message);
}
reader.close();//关闭流
socket.close();//关闭套接字
}
}catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
MyServer tcp = new MyServer();
tcp.start();//启动服务器
}
}
结果:
运行服务器端程序,将输出提示信息,等待客户呼叫。下面再来看一下客户端程序。
编写客户端程序,将用户在文本框中输入的信息发送至服务器端,并将文本框中输入的信息显示在客户端的文本域中。
package Package21;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class MyClient extends JFrame {
private PrintWriter writer;//根据套接字字节流创建的字符输出流
Socket socket;//客户端套接字
private JTextArea area = new JTextArea();//展示信息的文本
private JTextField text = new JTextField();//发送信息的文本框
public MyClient() {
setTitle("向服务器送数据");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container c = getContentPane();//著容器
JScrollPane scrollPane = new JScrollPane(area);//滚动面板
getContentPane().add(scrollPane,BorderLayout.CENTER);
c.add(text,"South");//将文本框放置在文本的下部
text.addActionListener(new ActionListener() {//文本框触发回车事件
public void actionPerformed(ActionEvent e) {
writer.println(text.getText().trim());//将文本框中的信息写入流
area.append(text.getText()+"\n");将文本框中的信息显示在文本域中
text.setText("");//将文本框清空
}
});
}
private void connect() {//连接服务器方法
area.append("尝试连接\n");//文本域中提示信息
try {
socket = new Socket("127.0.0.1",8998);//连接本地计算机的8998端口
writer = new PrintWriter(socket.getOutputStream(),true);
area.append("完成连接\n");
}catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
MyClient clien = new MyClient();
clien.setSize(200,200);//窗体大小
clien.setVisible(true);//显示窗体
clien.connect();//连接服务器
}
}
结果:
说明
当一台机器上安装了多个网络应用程序时,很可能指定的端口号已被占用。还可能遇到以前运行良好的网络程序突然运行不了的情况,这种情况很可能也是由于端口被别的程序占用了。此时可以运行netstat-help来获得帮助,使用netstat-an命令来查看该程序所使用的端口
21.3 UDP程序
用户数据报协议(UDP)是网络信息传输的另一种形式。基于UDP的通信和基于TCP的通信不同,基于UDP的信息传递更快,但不提供可靠性保证。使用UDP传递数据时,用户无法知道数据能否正确地到达主机,也不能确定到达目的地的顺序是否和发送的顺序相同。虽然UDP是一种不可靠的协议,但如果需要较快地传输信息,并能容忍小的错误,可以考虑使用UDP。
基于UDP通信的基本模式如下:
-
将数据打包(称为数据包),然后将数据包发往目的地。
-
接收别人发来的数据包,然后查看数据包。
发送数据包的步骤如下:
(1)使用DatagramSocket()创建一个数据包套接字。
(2)使用DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)创建要发送的数据包。
(3)使用DatagramSocket类的send()方法发送数据包。
接收数据包的步骤如下:
(1)使用DatagramSocket(int port)创建数据包套接字,绑定到指定的端口。
(2)使用DatagramPacket(byte[] buf, int length)创建字节数组来接收数据包。
(3)使用DatagramPacket类的receive()方法接收UDP包。
注意
DatagramSocket类的receive()方法接收数据时,如果还没有可以接收的数据,在正常情况下receive()方法将阻塞,一直等到网络上有数据传来,receive()方法接收该数据并返回。如果网络上没有数据发送过来,receive()方法也没有阻塞,肯定是程序有问题,大多数情况下是因为使用了一个被其他程序占用的端口号。
21.3.1 DatagramPacket类
java.net包的DatagramPacket类用来表示数据包。DatagramPacket类的构造方法如下:
- DatagramPacket(byte[] buf, int length)。
- DatagramPacket(byte[] buf, int length, InetAddress address, int port)。
第一种构造方法在创建DatagramPacket对象时,指定了数据包的内存空间和大小。第二种构造方法不仅指定了数据包的内存空间和大小,还指定了数据包的目标地址和端口。在发
送数据时,必须指定接收方的Socket地址和端口号,因此使用第二种构造方法可创建发送数据的DatagramPacket对象。
21.3.2 DatagramSocket类
java.net包中的DatagramSocket类用于表示发送和接收数据包的套接字。该类的构造方法如下:
- DatagramSocket()。
- DatagramSocket(int port)。
- DatagramSocket(int port, InetAddress addr)。
第一种构造方法创建DatagramSocket对象,构造数据报套接字,并将其绑定到本地主机任何可用的端口上。第二种构造方法创建DatagramSocket对象,创建数据报套接字,并将其绑定到本地主机的指定端口上。第三种构造方法创建DatagramSocket对象,创建数据报套接字,并将其绑定到指定的端口和指定的本地地址上。第三种构造函数适用于有多块网卡和多个IP地址的情况。
如果接收数据时必须指定一个端口号,不允许系统随机产生,此时可以使用第二种构造方法。比如有个朋友要你给他写信,那他的地址就必须确定,不确定是不行的。在发送数据时通常使用第一种构造方法,不指定端口号,而是系统为我们分配一个端口号,就像寄信不需要到指定的邮局去寄一样。
21.3.3 UDP网络程序设计
根据前面所讲的网络编程的基本知识以及UDP网络编程的特点,下面创建一个广播数据报程序。广播数据报是一项较新的技术,其原理类似于电台广播。广播电台需要在指定的波段和频率上广播信息,收听者也要将收音机调到指定的波段、频率,才可以收听广播内容。
【例21.3】创建UDP协议广播电台程序
package Package21;
import java.io.IOException;
import java.net.*;
public class Notification extends Thread {
String weather = "节目预报:八点有大型晚会,请收听";
int port = 9898;
InetAddress iaddress = null;
MulticastSocket socket = null;
@SuppressWarnings("deprecation")
Notification(){
try {
iaddress = InetAddress.getByName("224.255.10.0");//数字不能用中文数字
socket = new MulticastSocket(port);
socket.setTimeToLive(1);
socket.joinGroup(iaddress);
}catch(IOException e) {
e.printStackTrace();
}
}
public void run() {
while(true) {
DatagramPacket packet = null;
byte data[] = weather.getBytes();
packet = new DatagramPacket(data,data.length,iaddress,port);
System.out.println(weather);
try {
socket.send(packet);
sleep(3000);
}catch(IOException e) {
e.printStackTrace();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Notification W = new Notification();
W.start();
}
}
接收广播程序。单击“开始接收”按钮,系统开始接收主机播出的信息;单击“停止接收”按钮,系统停止接收广播主机播出的信息。代码如下:
package Package21;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;
public class Receive extends JFrame implements Runnable,ActionListener{
int port;
InetAddress group = null;
MulticastSocket socket = null;
JButton inceBtn = new JButton("开始接收");
JButton stopBtn = new JButton("停止接收");
JTextArea inceAr = new JTextArea(10,10);
JTextArea inced = new JTextArea(10,10);
Thread thread;
boolean stop = false;
public Receive() {
setTitle("广播数据报");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
thread = new Thread(this);
inceBtn.addActionListener(this);
stopBtn.addActionListener(this);
inceAr.setForeground(Color.blue);
JPanel north = new JPanel();
north.add(inceBtn);
north.add(stopBtn);
add(north,BorderLayout.NORTH);
JPanel center = new JPanel();
center.setLayout(new GridLayout(1,2));
center.add(inceAr);
center.add(inced);
add(center,BorderLayout.CENTER);
validate();
port = 9898;
try {
group = InetAddress.getByName("224.255.10.0");
socket = new MulticastSocket(port);
socket.joinGroup(group);
}catch(IOException e) {
e.printStackTrace();
}
setBounds(100,50,360,380);
setVisible(true);
}
public void run() {
while(!stop) {
byte data[] = new byte[1024];
DatagramPacket packet = null;
packet = new DatagramPacket(data,data.length,group,port);
try {
socket.receive(packet);
String message = new String(packet.getData(),0,packet.getLength());
inceAr.setText("正在接收的内容:\n"+message);
inced.append(message+"\n");
}catch(IOException e) {
e.printStackTrace();
}
}
}
public void actionPerformed(ActionEvent e) {
if(e.getSource()==inceBtn) {
inceBtn.setBackground(Color.red);
stopBtn.setBackground(Color.yellow);
if(!(thread.isAlive())) {
thread = new Thread(this);
}
thread.start();
stop = false;
}
if(e.getSource() == stopBtn) {
inceBtn.setBackground(Color.yellow);
stopBtn.setBackground(Color.red);
stop = true;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Receive rec = new Receive();
rec.setSize(460,200);
}
}
结果:
说明
发出广播和接收广播的主机地址必须位于同一个组内,地址范围为224.0.0.0~224.255.255.255,该地址并不代表某个特定主机的位置。加入同一个组的主机可以在某个端口上广播信息,也可以在某个端口上接收信息。