目录
[1. 服务器](#1. 服务器)
[1.1 实现逻辑](#1.1 实现逻辑)
[1.2 代码](#1.2 代码)
[1.3 部分代码解释](#1.3 部分代码解释)
[2. 客户端](#2. 客户端)
[2.1 实现逻辑](#2.1 实现逻辑)
[2.2 代码](#2.2 代码)
[2.3 客户端部分代码解释](#2.3 客户端部分代码解释)
[3. 程序运行结果](#3. 程序运行结果)
[4. 服务器客户端交互逻辑](#4. 服务器客户端交互逻辑)
此篇内容为实现UDP版本的回显服务器echo server;
普通服务器:收到请求,根据请求计算响应,返回响应;
回显服务器:忽略计算,直接将收到的请求作为响应返回;
(如需实现其他功能,修改响应计算方法process内容即可);
具体实现代码如下:
1. 服务器
1.1 实现逻辑
对于网络通信的服务器需要进行的工作为:
1. 读取请求并解析;
2. 根据请求计算响应;
3. 把响应写回客户端;
4. 打印交互详细信息;
1.2 代码
java
package TestDemo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoServer {
// 创建一个DatagramSocket对象,作为后续操作网卡的基础
private DatagramSocket socket = null;
public UDPEchoServer(int port) throws SocketException{
// 显式指定服务器端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
// 服务器启动方法
System.out.println("服务器启动");
// 为保证服务器随时向客户端提供服务
while(true){
// 1. 读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
// DatagramPacket对象用于承载从网卡读到的数据,收到数据时需要创建一个内存空间保存这个数据,
// DatagramPacket内部不能自行分配内存空间,需要程序员手动创建
socket.receive(requestPacket);
// 完成receive之后,数据以二进制形式存储在DatagramPacket中
// 需要将二进制数据转成字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
// 取0~requestPacket.getLength()区间内的字节构成一个String对象;
//2. 根据请求计算响应
String response = process(request);
//3. 把响应写回客户端
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4. 打印交互详细信息
System.out.printf("[%s:%d] req=%s, resp=%s\n",
requestPacket.getAddress().toString(),requestPacket.getPort(),
request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UDPEchoServer server = new UDPEchoServer(9090);
// 可以在1024~65535(临时端口)中任意选取端口号
server.start();
}
}
1.3 部分代码解释
- 为方便计算请求,将请求的DatagramPacket对象构造成字符串时只需获取DatagramPacket对象中实际有效的部分数据,requestPacket.getLength()获取到的就是收到数据的真实长度而非4096这个最大长度:
- 已经了解过UDP本身是无连接的,故而对于每一次通信过程,在构造数据报时都需要指定数据报要发给谁;
(1)对于构造的responsePacket对象有3个参数:
java
DatagramPacket responsePacket = new DatagramPacket(
response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress());
response.length()获取到的是响应对象字符串的字符个数,
response.getBytes().length获取到的是响应对象字符串的字节个数;
requestPacket.getSocketAddress()获取到的是请求发送方(即客户端)的IP与端口号;
(2)注意response.getBytes.length(字节个数)与response.getlength()(字符个数)的区别:
如果response字符串都是英文字符则二者相等,如果包含中文则二者不同;
在进行网络传输时,必然是需要通过字节为单位进行通信的。
- while(true)会使程序处于快速循环的状态,每循环一次就处理一次请求响应,
当客户端发出请求时,recerive就能顺利读取请求,客户端没有发出请求时,receive就会阻塞;
如果客户端发出的请求过多,可以使用多线程冲动调动计算机硬件资源,也可以再多开机器,但多开机器又会涉及到分布式问题;
- 使用格式化输出Packet的IP与端口号:
- 前文已经提及socket也是一个文件,但在上文代码中并未进行close操作,却没有造成文件资源泄漏的原因是:
socket是文件描述符表中的一个表项,每次打开一个文件就会占用一个位置,文件描述符在pcb上,是跟随进程的。在上文代码中创建的socket对象在整个程序运行过程中都需要使用,不可以提前关闭,当socket不需要使用时,即代表程序结束了,进程结束了,文件描述符表也销毁了,伴随着销毁都被系统自动回收了。故而不会造成文件资源泄露问题。
只有代码中频繁打开文件但不关闭,在一个进程的运行过程中,不断积累打开的文件,逐渐消耗掉文件描述符表中的内容,最后消耗殆尽,才会造成泄露。
对于生命周期很短的进程,无需考虑泄露,在客户端方一般来说影响不大。
2. 客户端
2.1 实现逻辑
对于网络通信的客户端,需要进行的工作是:
1. 从控制台读取数据作为客户端发出的请求;
2. 将请求字符串request构造成请求requestPacket对象,发送给服务器;
3. 尝试读取服务器返回的响应;
4. 将响应responsePacket对象构造成响应response字符串,显示出来;
2.2 代码
java
package TestDemo1;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UDPEchoClient {
private DatagramSocket socket = null;
private String serverIp = "";
private int serverPort = 0;
public UDPEchoClient(String ip, int port) throws SocketException {
// 客户端的socket对象需令系统自动分配
socket = new DatagramSocket();
// UDP本身不持有对端信息,需要在应用程序中记录对端信息(IP与端口)
serverIp = ip;
serverPort = port;
}
public void start() throws IOException {
// 客户端启动方法
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while(true){
// 1. 从控制台读取数据作为客户端发出的请求
System.out.println("->");
String request = scanner.next();
// 2. 将请求字符串request构造成请求requestPacket对象,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(
request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 将响应responsePacket对象转换为响应字符串response,显示出来
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();
}
}
2.3 客户端部分代码解释
1.DatagramPacket的三个构造方法:
第一种:只指定字节数组缓冲区,用于服务器接收请求与客户端接收响应时使用:
第二种:指定字节数组缓冲区与InetAddress对象(同时包含IP与端口),用于服务器向客户端发回响应时使用:
第三种:指定字节数组缓冲区,同时指定IP与端口号:
- 客户端对服务器发出请求的过程:
对于服务器端口必须是确定的:程序员可以手动分配空闲端口给当前服务器使用即可,
代码为:
java
socket = new DatagramSocket(port);
客户端一般都采取系统分配的方式:也可以指定端口,但不推荐。一方面,因为指定的端口可能被其他进程占用,如果被占用就会产生端口号冲突,运行就会抛出异常,提示绑定端口失败;另一方面,如果客户端出现了端口冲突,让客户手动解决也是不现实的。
代码为:
java
socket = new Datagramocket();
- 端口与进程的关系:
端口号用于标识或区分一个进程,因此在同一台主机上,不允许一个端口同时被多个进程使用;
但是一个进程可以绑定多个端口;
即socket和端口是一一对应的,进程与socket是一对多的;
3. 程序运行结果
首先启动EchoServer,再启动EchoClient,在客户端输入请求字符串后查看运行结果:
客户端与服务器通信成功。
4. 服务器客户端交互逻辑
-
服务器先启动,启动后进入循环,执行到receive处阻塞;
-
客户端开始启动后,进入循环执行scanner.next(),在此处阻塞。
当客户在控制台输入内容后,next返回作为请求,继而构造请求数据并发送给服务器;
- 客户端发送数据后,
服务器从receive中返回,解析请求构造字符串,执行process操作计算响应,构造响应后执行send发回给客户端;
客户端执行到receive处等待服务器的响应;
-
客户端获取到从服务器返回的数据后,从receive中返回,继而显示响应内容;
-
服务器完成一次循环后又执行到receive处,客户端完成依次循环后又执行到scanner.next处,二者均进入阻塞状态;
注意:当服务器程序在普通私有ip计算机上运行时,若不在一个局域网中,无法实现跨主机访问。
如果服务器程序在特殊的计算机:云服务器上,就拥有了公有ip,可以实现跨主机访问。
此部分内容后续详解。