Java笔记:《Java语言程序设计》–郭克华
UDP是面向无连接的,但并不是没有客户端和服务端的区别。只是说,服务端运行之后,并不一定要等待客户端的连接才能通信,客户端可以直接和服务端通信,发送信息。
这里介绍一个聊天应用最基本的程序:客户端和服务器相同,都可以给对方发送信息,也能够自动接收对方发过来的信息。本程序效果如图所示。
“服务器端”和“客户端”对话框中都有一个文本框,输入聊天信息。输入聊天信息之后,单击“发送”按钮,就能够将信息发送给对方,对方也能够自动收到之后显示。
显然这个程序在利用TCP实现双向聊天博客中也有介绍,阅读本篇博客之后,可以比较两者之间的差异。
在聊天程序中,各聊天的界面叫做客户端,客户端之间如果要相互聊天,则可以将信息先发送到服务器端,然后由服务器端转发。因此,客户端先要连接到服务器。客户端连接到服务器的IP地址和端口。在服务器端,必须监听某个端口。在客户端,必须连接服务器的某个端口,这点没有太大区别。
1.服务端怎样打开并监听端口?
UDP编程中,端口的监听是由java.net.DatagramSocket进行管理的,打开java.net.DatagramSocket的文档,这个类有很多构造函数,最常见的构造函数为:
public DatagramSocket(int port) throws SocketExceptin
传入一个端口号,实例化DatagramSocket。
ps:实例化DatagramSocket,就已经打开了端口号并进行监听。
例如,如下代码就可以监听服务器上的9999端口,并返回连接对象ds。
DatagramSocket ds=new DatagramSocket(9999);
2.客户端是怎样连接到服务器端的某个端口?
客户端连接到服务器端的某个端口也是由java.net.DatagramSocket进行管理的,打开java.net.DatagramSocket的文档,里面有一个重要函数:
public void connect(InetAddress address,int port)
传入一个封装了服务器IP地址的InetAddress对象和端口号。
ps:java.net.InetAddress有一个函数:
public static InetAddress getByName(String host) throws UnknownHostException
这是一个静态函数,可以传入一个IP地址,返回InetAddress对象。
例如,如下代码就可以连接服务器218.197.118.80上的9999端口。
DatagramSocket ds=new DatagramSocket();
InetAddress add=InetAddress.getByteName("218.197.118.80");
ds.connect(add,9999);
特别提醒
到这里为止,会发现这岂不是和TCP编程一样?是的,看起来一样,但是本质却不一样。在TCP编程中,接下来的工作就是服务器要获得客户端的连接,然后通过这个连接来进行通信。但是,在UDP编程中,这个工作不要了!就可以直接通信了!也就是说,服务器端不需要获得客户端的连接,它们就直接通过地址来收发信息。因此服务器不需要知道客户端是否连上来了。或说的更加直接一些,客户端以下代码:
InetAddress add=InetAddress.getByName("218.197.118.80");
ds.connect(add,9999);
实际上并没有连接服务器,只是将服务器的IP地址和端口保存起来,以后在客户端给服务器发送信息的时候,用这个IP地址和端口来寻找到服务器。
当然,从表面上可以理解成连接到服务器。
可以将TCP比喻成打电话,必须双方都拿起话机才能通话,并且连接要保持通畅。UDP比喻成寄信,在寄信的时候,对方根本不知道有信要寄过来,信寄到哪里去。靠信封上的地址。
以上所述还只是客户端连接到服务器端,接下来应该是客户端和服务器端的通信。通信包括读和写,对于客户端和服务器端,如果将数据传给对方,称为发送;反之,如果从对方处得到数据,为接收。
注意,前面讲解的TCP编程中,编程过程要用到输入输出流;在UDP情况下,不使用输入输出流,而采用数据包(DatagramPacket)的形式进行通信。对于一方来说,发送数据包,就称为输出;反之,接收数据包,就称为输入。
打开java.net.DatagramSocket文档,会发现里面有如下两个重要函数:
1.接收数据包:
public void receive(DatagramPacket p) throws IOException
2.发送数据包:
public void send(DatagramPacket p) throw IOException
这两个函数都传入一个对象:java.net.DatagramPacket,即数据包。
在文档中找到java.net.DatagramPacket,具有以下几个重要的构造函数:
1.创建一个DatagramPacket对象,指定内容和大小:
public DatagramPacket(byte[] buf,int length)
2.创建一个DatagramPacket对象,指定内容和大小,以及要发送的目标地址和端口号:
public DatagramPacket(byte[] buf,ing length,InetAddress address,int port)
很显然,第2个构造函数相当于给信封上写了寄信地址。
在这几个函数的参数中,已经发现,如果要发送或接收一个数据包,需要确定以下几个信息:
1.数据包所含数据,一般是一个字节数组。
2.数据包大小,可以确定为数据所占字节数。
3.数据包的发送地址,通过地址才能知道数据包发到哪里去。
如果要给对方发送数据包,数据包的发送地址必须是指定的,就如同寄信要指定收信人地址一样。怎样为数据包指定发送地址呢?规律如下:
1.客户端在确定服务器端IP地址的情况下,所创建的DatagramPacket对象,不需要设置发送地址,数据包可以直接发送给服务器端。
2.服务器事先不知道客户端的地址,因此,服务器必须手工指定发送地址,可以用前面讲解的第二个构造函数,也可以用DatagramPacket的如下函数:
(1)将DatagramPacket的发送地址设定为发送地址:
public void setAddress(InetAddress iaddr)
(2)将DatagramPacket的发送端口设定为指定端口:
public void setPort(int port)
这也从侧面说明,如果服务器要想客服端发送信息,但是又不知道客户端的地址,怎么办呢?一般说来,在通信时,必须客户端首先给服务器发送一个DatagramPacket,让服务器端利用这个数据包作为参考,知道客户端的地址,然后和该客户端通信;否则,服务器端就无法给客户端信息。
打开java.net.DatagramPacket文档,会发现还有如下函数:
1.设定长度:
public void setLength(int length)
2.设定数据:
public void setData(byte[] buf)
3.得到数据包中对方的地址:
public InetAddress getAddress()
4.得到数据包中对方的端口:
public int getPort()
5.得到对方地址和端口的封装:
public SocketAddress getSocketAddress()
6.得到数据:
public byte[] getData()
7.得到长度:
public int getLength()
如下代码表示客户端向服务器端发送一个数据包:
DatagramSocket ds=new DatagramSocket();
InetAddress add=InetAddress.getByName("127.0.0.1");
ds.connect(add,9999);
String msg=("服务器,你好");
byte[] data=msg.getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.send(dp);
如下代码表示服务器获得客户端发送过来的数据包,并打印其内容:
DatagramSocket ds=new DatagramSocket(9999);
byte[] data=new byte[255];
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.receive(dp);
String msg=new String(dp.getData(),0,dp.getLength());
System.out.println("已经收到:"+msg);
从这里可以总结出UDP数据通信的过程:
1.服务器端监听端口。
2.客户端“连接”服务器端。
3.一端创建一个DatagramPacket对象,设定其大小、数据和发送地址;然后用DatagramSocket的send函数发出。
4.另一端用DatagramSocket的receive函数读取DatagramPacket对象。
5.获取DatagramPacket中的数据。
ps:值得一提的是,在客户端与服务器端之间传递信息时,DatagramSocket的receive函数是一个“死等函数”,如果客户端端连接上了,但是没有发送信息,它会一直等待。
因此,客户端和服务器端如果需要自动读取对方传来的信息,就不能将receive函数放在主线程内,因为在不知道对方在什么时候会发出信息的情况,receive函数的死等,可能会造成程序的阻塞。所以,最好的方法是将读取信息的代码写在专门的线程内。
综合以上叙述,建立服务器端代码如下:
package practice;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import javax.swing.*;
public class Server extends JFrame implements ActionListener,Runnable {
private JTextArea taMsg=new JTextArea("以下是聊天记录\n");
private JTextField tfMsg=new JTextField("请您输入信息\n");
private JButton btSend=new JButton("发送");
private DatagramSocket ds=null;
//保存好客户端的地址和端口
private SocketAddress cAddress=null;
public Server() {
this.setTitle("服务器端");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.add(taMsg,BorderLayout.CENTER);
tfMsg.setBackground(Color.yellow);
this.add(tfMsg,BorderLayout.NORTH);
this.add(btSend,BorderLayout.SOUTH);
btSend.addActionListener(this);
this.setSize(200,300);
this.setVisible(true);
try {
ds=new DatagramSocket(9999);
new Thread(this).start();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
@Override
public void run() {
try {
while(true) {
byte[] data=new byte[255];
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.receive(dp);
//保存客户端地址
cAddress=dp.getSocketAddress();
String msg=new String(dp.getData(),0,dp.getLength());
taMsg.append(msg+"\n");//添加内容
}
}
catch (Exception ex) {
}
}
@Override
public void actionPerformed(ActionEvent e) {
try {
String msg="客户端说:"+tfMsg.getText();
byte[] data=msg.getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.send(dp);
}
catch (Exception ex) {
}
}
public static void main(String[] args) {
new Server();
}
}
运行这个程序就可以得到服务器的效果。
接下来是客户端程序,代码如下:
package practice;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import javax.swing.*;
public class Client extends JFrame implements ActionListener,Runnable {
private JTextArea taMsg=new JTextArea("以下是聊天记录\n");
private JTextField tfMsg=new JTextField("请您输入信息");
private JButton btSend=new JButton("发送");
private DatagramSocket ds=null;
public Client(){
this.setTitle("客户端");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.add(taMsg,BorderLayout.CENTER);
tfMsg.setBackground(Color.yellow);
this.add(tfMsg,BorderLayout.NORTH);
this.add(btSend,BorderLayout.SOUTH);
btSend.addActionListener(this);
this.setSize(200,300);
this.setVisible(true);
try {
ds=new DatagramSocket();
InetAddress add=InetAddress.getByName("127.0.0.1");
ds.connect(add,9999);
String msg="客户端连接";
byte[] data=msg.getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.send(dp);
new Thread(this).start();
}
catch (Exception ex) {
}
}
@Override
public void run() {
try {
while (true) {
byte[] data=new byte[255];
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.receive(dp);
String msg=new String(dp.getData(),0,dp.getLength());
taMsg.append(msg+"\n"); //添加内容
}
}
catch (Exception ex) {
}
}
@Override
public void actionPerformed(ActionEvent e) {
try {
String msg="客户端说:"+tfMsg.getText();
byte[] data=msg.getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.send(dp);
}
catch (Exception ex) {
}
}
public static void main(String[] args) {
new Client();
}
}
运行得到客户端界面,两者即可进行聊天。
注意
1.必须要先运行服务器端,在运行客户端。
2.如果客户端关掉之后重新开启,也可以继续聊天。在TCP的双向聊天中,这是办不到的。