17.网络通信
网络应用程序可以让不同计算机之间交换数据。编写网络应用程序,首先必须明确网络应用程序索要使用的网络协议,TCP/IP协议是网络应用程序的首选。
17.1网络程序设计基础
Java已经将网络程序所需要的东西封装成不同的类。
17.1.1局域网与英特网
为了实现两台计算机之间的通信,必须要用一个网络连接服务器和客户机。服务器是指提供信息的计算机或者程序。客户机是指请求信息的计算机或程序。
因特网,由无数的LAN(Local Area Network,局域网)和WAN(Wide Area Network,WAN)组成的。
17.1.2网络协议
网络协议规定了计算机之间连接的物理、机械(网线与网卡的连接规定)、电气(有效的电平范围)等特征和计算机之间的相互寻址规则、数据发送冲突的解决、长的数据如何分段传送与接受等。常用网络协议如下:
1.IP协议
IP是Internet Protecol 的简称,是一种网络协议。Internet网络采用的协议是TCP/IP协议,全称是Transmission Control Protocol?Internet Protocol。
在Internet网络上,每台主机用为其分配的Internet地址代表自己,即IP地址。目前IP地址用4个字节,32位2进制数表示,称为IPv4,目前人们正在实验用16个自己表示IP地址,即IPv6。
TCP/IP协议是一种层次结构,共分为4层,分别是应用层、传输层、互联网层和网络层。
2.TCP与UDP协议
在TCP/IP协议栈中,有两个重要的高级协议,分别是传输控制协议(Transmission Control Protocol,TCP)和用户数据报协议(User Datagram Protocol,UDP)。
1.TCP
TCP协议提供两台计算机之间可靠的数据传送。确保数据能够准确送达,而且抵达的数据排列顺序和发送顺序相同。HTTP、FTP和Telnet等都需要使用可靠的通信频道, 所以都是基于TCP实现的。
2.UDP
UDP是无连接通信协议。不保证可靠的数据传输。能够想若干个目标发送数据,接受来自若干个源的数据。UDP是以独立发送数据包的方式进行。UDP协议适用于一些对数据准确性要求不高的场合,如网络聊天室、在线影片等。
值得注意的是,一些防火墙和路由器会设置成不允许UDP数据包传输。因此,遇到UDP连接方面的问题时,应首先确定所在网络是否允许UDP协议。
17.1.3端口和套接字
1.端口
通常一台计算机只有单一的连接到网络的物理接口(Physical Connection),所有数据都通过此连接对内、对外送达特定的计算机,即端口。端口并非是一个物理装置,而是通过软件实现的一个连接装置。
端口被规定为一个在0~65535之间的整数,其中0~1023之间的端口用于一些知名的网络服务和应用。
客户端会通过不同的端口确定连接到服务器的哪项服务上,如HTTP服务一般使用80端口,FTP使用21端口。
2.套接字
套接字(Socket)用于连接应用程序和端口。也是一个虚拟的连接装置。
Java将套接字抽象化为类,程序设计者只需创建Socket类对象,即可使用套接字。
17.2TCP程序设计基础
TCP网络程序设计是指利用Socket类编写通信程序。服务器端和客户端交互过程如下所示:
1.服务器程序创建一个ServerSocket(服务器端套接字),调用accept()方法等待客户机来连接
2.客户端程序创建一个Socket,请求与服务器建立连接
3.服务器接受客户机的连接请求,同时创建一个新的Socket与客户端建立连接,服务器继续等待新的请求
17.2.1 InetAddress类
java.net包中的InetAddress类是与IP地址相关的类,利用该类可以获取IP地址、主机地址等信息。常用方法如下:
方法 返回值 功能说明
getByName(String host) InetAddress 获取与Host相对应的InetAddress对象
getHostAddress() String 获取InetAddress对象所包含的IP地址
getHostName() String 获取此IP地址的主机名
getLocalHost() InetAddress 返回本地主机的InetAddress对象
举例如下:
import java.net.*;
public class Address {
public static void main(String[] args) {
try {
InetAddress ip = InetAddress.getLocalHost();
String LocalName = ip.getHostName();
String LocalIp = ip.getHostAddress();
System.out.println("本机名" + LocalName);
System.out.println("本机IP地址" + LocalIp);
/*InetAddress ip1;
ip1 = InetAddress.getByName("");
//在参数中输入其他主机的主机名,可以获取其他主机的IP地址
LocalIp = ip1.getHostAddress();
System.out.println("该主机的IP地址为"+LocalIp);*/
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
InetAddress类的方法可能抛出UnknownHsotException异常,所以必须进行异常处理,这个异常在主机不存在或者网络连接错误时发生。
17.2.2ServerSocket类
java.net包中的ServerSocket类用于表示服务器套接字,服务器套接字通过指定的端口来等待连接的套接字。
服务器套接字一次可以与一个套接字连接。当多台客户机同时发出请求,服务器套接字会将客户机存于队列,从中取出一个套接字,进行连接。
如果请求连接数大于队列最大容纳数,则多出来的连接请求被拒绝。队列默认大小是50。
SerberSocket类的构造方法都抛出IOException异常,分别有以下几种形式:
1.ServerSocket():创建非绑定服务器套接字
2.ServerSocket(int prot):创建绑定到特定端口的服务器套接字
3.ServerSocket(int port,int backlog):利用指定的backlog创建服务器套接字,并绑定到指定的本地端口号
4.ServerSocket(int port,int backlog,InetAddress bindAddress):使用指定的端口、监听backlog和要绑定到的本地IP地址创建服务器。该构造方法适用于计算机上有多块网卡和多个IP地址的情况,用于可以明确规定ServerSocket在哪块网卡或IP地址上等待客户的连接请求。
ServerSocket类常用方法如下所示:
方法 返回值 功能说明
accept() Socket 等待客户机的连接。若连接,创建一个套接字
isBound() boolean 判断ServerSocket的绑定状态
getInetAddress() InetAddress 返回此服务器套接字的本地地址
isClosed() boolean 返回服务器套接字的关闭状态
close() void 关闭服务器套接字
bind(SocketAddress endpoint) void 将ServerSocket绑定到特定地址(IP地址和端口号)
getInetAddress() int 返回服务器套接字等待的端口号
调用ServerSocket类的accept()方法会返回一个和客户端Socket对象相连接的Socket对象。此时服务器端Socket对象和客户端Socket对象通过输入输出流互相连接。(服务器端的Socket对象使用getOutputStream()方法获得的输出流将指向客户端Socket对象使用getInputStream()方法获得的输入流;服务器端的Socket对象使用getInputStream()方法获得的输入流将指向客户端Socket对象使用getOutputStream()方法获得的输出流)
accept()方法会阻塞线程的继续执行,直到接到客户端的呼叫。如果客户端没有呼叫服务器,那么System.out.println(”连接中“)语句就不会执行。但如果没有收到客户请求,accept()方法没有发生阻塞,肯定是程序处理问题,通常是使用了一个还在被其他程序占用的端口号。ServerSocket绑定没有成功。
17.2.3TCP网络程序
单向通信只要求客户端向服务端发送消息,而不需要服务端向客户端发生信息。要实现单向通信,只需要客户机套接字和服务器套接字连接成功后,客户机通过输出流发送数据,服务器通过输入流接受数据即可,举例如下:
以下是一个TCP服务器端程序,在getserver()方法中建立服务器套接字,带哦用getClientMessage()方法获取客户端信息:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class MyTCP {
private BufferedReader reader;
private ServerSocket server;
private Socket socket;
void getserver() {
try {
server = new ServerSocket(18998);
System.out.println("服务器套接字已经创建成功");
while (true) {
System.out.println("等待客户机连接");
socket = server.accept();
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("");
getClientMessage();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void getClientMessage() {
try {
while (true) {
System.out.println("客户机" + reader.readLine());
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (reader != null) {
reader.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyTCP tcp = new MyTCP();
tcp.getserver();
}
}
以下是客户端程序,实现将用户在文本框中输入的信息发送至服务器端,并将文本框中输入的信息显示在客户端文本域中。
import javax.swing.*;
import javax.swing.border.BevelBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.net.Socket;
public class MyClient extends JFrame {
private PrintWriter writer;
Socket socket;
private JTextArea ta=new JTextArea();
private JTextField tf=new JTextField();
Container cc;
public MyClient(String title){
super(title);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
cc=this.getContentPane();
final JScrollPane scrollPane=new JScrollPane();
scrollPane.setBorder(new BevelBorder(BevelBorder.RAISED));
getContentPane().add(scrollPane,BorderLayout.CENTER);
scrollPane.setViewportView(ta);
cc.add(tf,"South");
tf.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
writer.println(tf.getText());
ta.append(tf.getText());
ta.setSelectionEnd(ta.getText().length());
tf.setText("");
}
});
}
private void connect(){
ta.append("尝试连接");
try{
socket =new Socket("127.0.0.1",18998);
writer =new PrintWriter(socket.getOutputStream(),true);
ta.append("完成连接\n");
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
MyClient client=new MyClient("向服务器发送数据");
client.setSize(200,200);
client.setVisible(true);
client.connect();
}
}
对于端口被占用的情况,可以在windows命令行中使用命令netstat -an查看。
17.3UDP程序设计基础
相比TCP,基于UDP(用户数据报协议)的信息传递更快,但是不提供可靠的交付。
用户无法知道数据能否正确到达主机,也不能确定到达的目的地的顺序是否和发送的顺序相同。
对于需要较快地传输信息,并且能忍受小的错误,可以考虑采用UDP传输。
以下是总结的UDP程序的步骤。
发送数据包:
(1)使用DatagramSocket()创建一个数据包套接字。
(2)使用DatagramPakcet(byte[] buf,int offset,int length,InetAddress address,int port)创建要发送的数据包。
(3)使用DatagramSocket类的send()方法发送数据包。
接受数据包:
(1)使用DatagramSocket(int port)创建数据包套接字,绑到指定的端口。
(2)使用DategramPacket(byte[] buf,int length)创建字节数组来接受数据包。
(3)使用DatagramPacket类的receiver()方法来接受UDP包。
值得注意的是,在没有可接受的数据时,receive()方法将阻塞,一道有数据传来,receive()方法接受该数据并返回。如果没有数据时receive()方法没有阻塞,多半是使用了一个被其它程序占用的端口号。
17.3.1DatagramPacket类
java.net包的DatagramPacket类用来表示数据包。构造函数如下:
Datagrampacket(byte[] buf,int length)
DatagramPacket(byte[] buf,int length,InetAddress address,int port)
第一种构造函数指定了数据包内存空间和大小。
第二种构造函数指定了数据包内存空间和大小,还指定了数据包的目标地址和端口。发送数据时必须指定接收方的Socket地址和端口号,因此使用第二种构造方法可以创建发送数据的DatagramPacket对象。
17.3.2DatagramSocket类
java.net包中的DatagramSocket类用于表示发送和接受数据包的套接字,构造函数如下:
DatagramSocket()
DatagramSocket(int port)
DatagramSocket(int port,InetAddress addr)
第一种构造方法创建DatagramSocket对象,构造数据包套接字并将其绑定到本地主机上任何可用的端口。
第二种构造方法创建DatagramSocket对象,创建数据包套接字并将其绑定到本地主机上指定端口。
第三种构造方法创建DatagramSocket对象,创建数据包套接字并将其绑定到指定的本地地址。该构造方法适用于有多块网卡和多个IP地址的情况。
发送程序中,通常使用第一种构造方法,不指定端口号,由系统自动分配。
接收程序必须指定一个端口号,通常使用第二种构造方法。
17.3.3UDP网络程序
以下是一个广播数据包程序。主机不断重复播出节目预报,可以保证加入到同一组的主机随时可以接收到广播消息。接收者将正在接受的信息放在一个文本域中,并将接受的全部信息放在另外一个文本域中。
广播主机程序不断向外播出信息,代码如下:
import java.net.*;
public class Weather extends Thread {
String weather="节目预报:八点有大型晚会,请收听";
int port=9898;
InetAddress iaddress=null;
MulticastSocket socket=null;
Weather(){
try{
//实例化InetAddress,指定地址
iaddress=InetAddress.getByName("224.225.10.0");
socket=new MulticastSocket(port);
socket.setTimeToLive(1);
socket.joinGroup(iaddress);
}catch(Exception 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(new String(data));
try{
socket.send(packet);
sleep(3000);
}catch(Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Weather w=new Weather();
w.start();
}
}
接受广播程序:单击”开始接受”按钮,系统开始接受主机播出的信息;单击”停止接受”按钮,系统会停止接受广播主机播出的信息。
import javax.swing.*;
import java.awt.event.*;
import java.net.*;
import java.awt.*;
public class Receive extends JFrame implements Runnable, ActionListener {
int port;
InetAddress group = null;
MulticastSocket socket = null;
JButton ince = new JButton("开始接收");
JButton stop = new JButton("停止接受");
JTextArea inceAr = new JTextArea(10, 10);
JTextArea inced = new JTextArea(10, 10);
Thread thread;
boolean b = false;
public Receive() {
super("广播数据报");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
thread = new Thread(this);
ince.addActionListener(this);
stop.addActionListener(this);
inceAr.setForeground(Color.blue);
JPanel north = new JPanel();
north.add(ince);
north.add(stop);
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(Exception e){
e.printStackTrace();
}
setBounds(100,50,360,380);
setVisible(true);
}
public void run(){
while(true){
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(Exception e){
e.printStackTrace();
}
if(b==true){
break;
}
}
}
public void actionPerformed(ActionEvent e){
if(e.getSource()==ince){
ince.setBackground(Color.red);
stop.setBackground(Color.yellow);
if(!(thread.isAlive())){
thread=new Thread(this);
}
thread.start();
b=false;
}
if(e.getSource()==stop){
ince.setBackground(Color.yellow);
stop.setBackground(Color.red);
b=true;
}
}
public static void main(String[] args) {
Receive rec=new Receive();
rec.setSize(460,200);
}
}
要广播或者接受广播的主机地址必须加入到一个组内,地址范围为224.0.0.0~224.255.255.255,这类地址并不代表某个特定主机的位置。加入到同一组的主机可以在某个端口上广播信息,也可以在某个端口上接受信息。