Java 网络编程(一)—— UDP数据报套接字编程
概念
在网络编程中主要的对象有两个:客户端和服务器。客户端是提供请求的,归用户使用,发送的请求会被服务器接收,服务器根据请求做出响应,然后再将响应的数据包返回给客户端。
作为程序员,我们主要关心应用层和传输层,我们编写的程序属于应用层,需要调用传输层的接口来进行数据的传输。Java给我们提供了两套接口,一套是属于UDP 协议的,另一套是属于 TCP 协议的。本篇文章重点讲解UDP 数据报套接字编程。
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
基于Socket套接字的网络程序开发就是网络编程。
UDP数据报套接字
DatagramSocket
DatagramSocket 简单来理解就是定位你所在的位置,用于接收和发送数据报
构造方法:
方法名 | 说明 |
---|---|
DatagramSocket() | 无参构造方法,不指定端口号,由操作系统自行分配 |
DatagramSocket(int port) | port 就是端口号,这个构造方法就是由程序员自行指定端口号 |
接收和发送数据包的方法:
方法名 | 说明 |
---|---|
send(DatagramPacket p) | 发送数据包 |
receive(DatagramPacket p | 接收数据包,这里是输出型参数,传输层把数据内容填充到你传入的数据包中 |
什么是输出型参数?
该参数在方法内部会被改变,并且会影响到方法外部的实参。
关闭方法:
方法名 | 说明 |
---|---|
close() | 关闭资源 |
注意了网络编程使用 Socket ,也是和内存、文件一样都会消耗资源的。
DatagramPacket
DatagramPacket 就是数据报,也就是你发送和接收的数据报,这里数据报和数据包不作区分,大家知道就好了。
构造方法:
字节数组就是用来填充数据的,也是输出型参数。
offset 是指定偏移量
length 是指定要填充多少个字节
address 就是传入地址,SocketAddress 就是一个完整的地址(包含IP 和端口号),InetAddress 只是包含 IP地址,port 就是我们熟悉的端口号。
其他方法:
方法名 | 说明 | 返回值 |
---|---|---|
getAddress() | 获得该数据包的 IP 地址 | InetAddress |
getPort() | 获得该数据包端口号 | int |
getSocketAddress() | 获得该数据包的完整地址(包含IP地址和端口号) | SocketAddress |
getData() | 或者数据内容 | byte[] |
getLength() | 获得数据的长度,以字节为单位 | int |
InetSocketAddress
构造方法:
方法名 | 说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个 Socket 地址,包含IP地址和端口号 |
其他方法:
方法名 | 说明 | 注意 |
---|---|---|
getByName(String host) | 将主机名转化为机器能识别的IP地址 | 静态方法 |
这个方法有什么用?
我们知道一个IP地址我们习惯用十进制来表示,类似”xxx.xxx.xxx.xxx",我们通常传入的这个IP地址是一个字符串,这个方法就能将这个字符串转化为机器能识别的二进制的 IP 地址。
回显服务器编写
这里简单介绍一下,回显服务器就是你发什么我就回什么,例如客户端发送一个 hello,服务器直接返回 hello,这就是回显服务器,此服务器是用来我们学习套接字的。现在我来带领大家完成服务器代码的编写。
首先创建一个服务器的类,这里定义为 UdpEchoServer,Echo 就是回显的意思。
在类里面先定义字段 DatagramSocket socket
private DatagramSocket socket;
//给服务器指定一个端口号
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
在构造方法这里要指定对应的端口号,服务器的位置一定要固定下来,防止客户端那边找不到服务器。
启动程序
服务器是 7 * 24 小时为用户提供的服务的,所以这里我们直接写一个死循环 while(true) {}
每一次循环都是在处理一次请求。
首先我们要接收客户端的数据包,先创建好一个空的数据包来接收数据,这里为什么不传入地址,因为我们这个数据包只是用来接收数据的,并且就在服务器中使用,不需要添加地址。
//构建请求数据包
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
然后接收:
//获取请求
socket.receive(requestPacket);//输出型参数
如果没有数据可以接收的话,服务器程序会一直在这里阻塞住。
解析数据包并计算请求,由于这里是回显服务器,所以我们直接构造出 String,然后形式上进行响应的处理:
//解析请求数据包
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//计算响应值
//这里是回显服务器,直接返回原数据
String response = process(request);
//计算响应,服务器的核心代码区域
private String process(String request) {
return request;
}
在真实的服务器代码中,我们在响应这里的处理是服务器的核心逻辑,由于是回显服务器,也就显得没有什么感受。
之后就要把响应的数据包发送回客户端那边。
注意:由于 UDP 是不会保存对端的 IP地址和端口号的,所以我们在构建响应数据包的时候,一定要传入目的 IP 和 目的端口号,这里的目的 IP 和 目的端口号可以从请求的数据包获取,因为请求的数据包保存了客户端的IP 和 端口号。
//构建响应数据包
//UDP 不存放对端的源 IP 和 源端口,所以需要传入对方的地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
//发送响应数据包
socket.send(responsePacket);
还要注意数据的长度一定是response.getBytes().length
,不要写出字符串的长度,因为我们的数据是字节,字节的大小和字符的大小是不一样的。
最后我们可以打印一个日志:
//打印日志
System.out.printf("[%s : %d] request: %s response: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
因为 getAddress() 的返回值是 InetAddress ,所以要使用 toString() 转化为字符串进行打印。
最终代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//服务器程序
public class UdpEchoServer {
private DatagramSocket socket;
//给服务器指定一个端口号
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//服务器启动运行程序
public void start() throws IOException {
System.out.println("服务器启动...");
//服务器持续运行
//每次循环处理一次请求
while(true) {
//构建请求数据包
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//获取请求
socket.receive(requestPacket);//输出型参数
//解析请求数据包
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//计算响应值
//这里是回显服务器,直接返回原数据
String response = process(request);
//构建响应数据包
//UDP 不存放对端的源IP 和 源端口,所以需要传入对方的地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
//发送响应数据包
socket.send(responsePacket);
//打印日志
System.out.printf("[%s : %d] request: %s response: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//计算响应,服务器的核心代码区域
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
客户端编写
客户端是一定要知道请求是发到哪一个服务器上的,所以我们需要保存好服务器的IP 地址和端口号。
private DatagramSocket socket;
private String serverIP;
private int serverPort;
构造方法:注意一定要传入服务器的 IP地址 和 端口号,socket 使用的是 DatagramScoket 的无参构造方法。
为什么使用的是 DatagramScoket 的无参构造方法?
首先我们作为程序员不知道用户那边的主机的端口使用情况,如果固定用户的端口号,正好用户此时已经有进程占用了这个端口号,这时候我们的客户端程序是跑不起来的,这就是端口冲突。
为了避免端口的冲突,我们不指定端口号,而是交给用户主机的操作系统自行指定端口号。
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
this.serverPort = serverPort;
this.serverIP = serverIP;
socket = new DatagramSocket();//不用指定客户端的端口号,让用户自己的操作系统自己去安排端口号,避免端口号冲突
}
启动程序
这里我们直接让用户从控制台输入要发送的数据,我们构建好请求数据包并发送到服务器上
//构建请求数据包
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
//发送数据包
socket.send(requestPacket);
这里我们就使用了InetAddress.getByName()
这个方法将本身是 String 类型的IP地址转化为机器能识别的IP地址。
然后要注意数据的长度一定是request.getBytes().length
,不要写出字符串的长度,因为我们的数据是字节,字节的大小和字符的大小是不一样的。
最后就是传入目的地址也就是服务器的源IP和源端口号,然后发送给服务器那边。
接着就是接收服务器的响应,我们构建一个空的响应数据包来接收:
//接收数据包
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
最后就是解析并打印响应内容了。
//打印响应数据
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println("响应:" + response);
最终代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//客户端程序
public class UdpEchoClient {
private DatagramSocket socket;
private String serverIP;
private int serverPort;
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
this.serverPort = serverPort;
this.serverIP = serverIP;
socket = new DatagramSocket();//不用指定客户端的端口号,让用户自己的操作系统自己去安排端口号,避免端口号冲突
}
public void start() throws IOException {
System.out.println("欢迎来到客户端...");
Scanner scan = new Scanner(System.in);
while(true) {
//用户从控制台输入数据
System.out.println("请输入你要发送的数据:");
while(!scan.hasNext()) {
break;
}
String request = scan.nextLine();
//构建请求数据包
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
//发送数据包
socket.send(requestPacket);
//接收数据包
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//打印响应数据
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println("响应:" + response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
由于我这里是使用一台主机的两个进程来模拟客户端和服务器的,所以IP地址指定为"127.0.0.1",这是每台主机默认的IP地址,端口号这里指定为 9090,最后大家运行两个程序,就可以看到下面的效果了。
效果展示: