前言
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
这篇博客会解析基于UDP协议来进行网络编程,进行客户端和服务器之间的交互,主要是把客户端和服务器之间交互的流程走一遍,包括发送数据报、读取数据报、将字符串数据转为数据报,读取到数据报转化为字符串
铁汁们可以看上篇“初始网络通信”博客,复习下网络通信的基础知识,初学时对服务器和客户端交互的代码可能会感觉比较抽象,建议多写几次代码,结合api解析和数据传输流程细品下🐵
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的JavaEE专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~
1,网络编程简介
描述网络通信时传输的数据,可以说数据报/段/包/帧,4种叫法都是网络传输数据的基本单位,在日常工作中,一般是不去区分这四种的区别的,是混着叫的
但是严谨来说,这四种叫法还是有区别的,在不同场景下有不同的叫法(写学术论文时才需要注意,这里铁汁们了解下即可):
- 报(datagram):UDP协议
- 段(segment):TCP协议
- 包(packet):IP协议
- 帧(frame):数据链路层的协议
学习网络编程,写的程序是处于应用层上的,程序员主要的工作就是调用系统的api(对应的功能接口),把数据交给传输层,至于传输层到物理层之间的通信逻辑,都是由操作系统/驱动/硬件已经实现好的,不需要程序员关心了
网络编程重点要学习的就是传输层的api,传输层提供的api又称为socket api(网络编程套接字),这个socket的翻译是插槽,应用程序创建 Socket,就相当于在本机上开辟一个通信端点,把程序 “接入” 网络,完成数据收发。
传输层主要用到的协议就是TCP和UDP协议,这两个协议提供了两组socket api,
2,TCP和UDP
TCP和UDP区别如下:
- TCP:有连接,可靠传输,面向字节流,全双工
- UDP:无连接,不可靠传输,面向数据报,全双工
有连接和无连接的理解,可以类比结婚,结婚的标志是两个人领了结婚证,就相当于建立了连接,两个人通过结婚证保存了对方的关键信息,通过结婚证,就能知道小帅的老婆是小美,小美的老公是小帅
网络上建立的是一种抽象的连接,有无连接就是看双方是否保存了对方的关键信息,TCP双方就会互相保存对方的信息,UDP双方不会保存对方的信息(如果需要保存,还需要在应用层写代码来保存)
这个可靠/不可靠传输,就跟丢包有关,丢包是客观存在的,可靠传输就是在对抗丢包
- TCP就是尽可能的确保数据能够被对方接收到,属于可靠传输
- UDP则是把数据包发出去后,就不再管了
注:一些文章中可能会把这个可靠性和安全性混淆了,说TCP传输是安全的,UDP传输是不安全的,因为TCP尽可能确保数据能被对方接收到,这个跟安全性没关系,安全是指数据不容易被黑客截获/破解
UDP传输的是数据报,TCP传输的是字节流,全双工指的是通信双方可以同时发送和接收数据,收发互不干扰
3,UDP编程api
UDP编程首先要创建个DatagramSocket对象,这个对象就代表了操作系统中的socket文件,按照之前的理解,文件就是硬盘的抽象,读写文件时,本质上就是在操作硬盘,但文件在操作系统中是更广义的概念,代表着更多的硬件设备和软件资源
网卡是计算机和网络之间的硬件接口,负责把电脑里的数字信号转换成网线 / Wi‑Fi 能传输的信号,同时接收网络上的数据,转回计算机能识别的数字信号。
网卡硬件的设备也是通过文件来封装的,通过网络发送数据,需要往网卡中写入,通过网络接收数据,需要从网卡中读取,封装网卡的文件,就称为"socket文件"
这个文件是先打开,再读写,最后关闭,跟前面文件IO的一些设定是非常类似的,Java中创建一个DatagramSocket对象,就是在操作系统中打开了一个socket文件(代表网卡),通过这个对象写入数据,就是在通过网卡发送数据;通过这个对象读取数据,就是在通过网卡接收数据
Datagram类中的方法介绍如下(后面写代码时还会解析):
- DatagramSocket()
无参构造方法,创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) - DatagramSocket(int port)
有参构造方法,创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) - void receive(DatagramPacket p)
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) - void send(DatagramPacket p)
从此套接字发送数据报包(不会阻塞等待,直接发送) - void close()
关闭此数据报套接字
UDP协议中,发送和传输的都是数据报,UDP的数据报类就是DatagramPacket,数据报是UDP通信时的基本传输单位
DatagramPacket对象本质上存储的是字节数组,也就是把应用数据编码转换成字节数组,存到DatagramPacket对象中
DatagramPacket类中的方法介绍如下:
-
DatagramPacket(byte[] buf, int length)
构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在字节数组(第一个参数 buf)中,接收指定长度(第二个参数 length) -
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节数组(第一个参数 buf)中,从 0 到指定长度(第二个参数 length)。address 指定目的主机的 IP 和端口号 -
InetAddress getAddress()
从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址 -
int getPort()
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 -
byte[] getData()
获取数据报中的数据
网络通信的基本流程如下图,客户端主动发起通信,服务器被动的接收通信,通常是一个服务器要服务多个客户端的,因为是基础部分,一会写代码就先跑通一台服务器,一个客户端的情况

客户端和服务器要执行的任务和执行顺序如下图:

4,UDP回显服务器/客户端
1,服务器
我们就用UDP协议做个简单的“回显服务器”(EchoServer),回显服务器省略了服务器根据请求计算响应的部分,把客户端发送的数据再原样返回给客户端
首先写下服务器的代码,定义个DatagramSocket的成员变量,写个构造方法,里面传入port,也就是端口号,创建DatagramSocket对象,创建的时候传入端口号进行绑定,此时这个socket就关联到了一个端口号
端口号是上篇博客中提到的五元组中的一部分,想发送信息,就要知道目标端口号和源端口号
例如服务器在启动时,绑定一个端口号6060,客户端在启动时绑定一个端口号9090,客户端在给服务器发送请求时,源端口是9090,目标端口是6060;
服务器给客户端发送响应时,源端口是6060,目标端口是9090
端口号在网络协议中,是用两个字节表示的无符号整数,范围就是0 - 65535,0 - 1024这个范围的端口号,称为“知名端口号”,系统预留给了一些知名协议的服务器来使用,因此铁汁们绑定端口号的时候就不要绑定这个范围的
package network;
import java.net.DatagramSocket;
import java.net.SocketException;
public class EchoServer{
private DatagramSocket socket;
public EchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
}
规定一个端口在同一时刻,只能被一个进程绑定,假如steam进程和B站进程绑定了同一个端口号,那传输回来的游戏数据也会给B站应用,这样就乱套了,因此如果尝试给socket绑定已占用的端口号,这里就会抛出异常
铁汁们绑定端口号时,如果抛出异常了,换个端口号绑定即可
也可以打开命令行工具,输入netstat -an,查看当前的端口占用情况,本地地址中冒号后面的部分就是端口号,6万多个端口号,这里也没占用多少,因此绑定时重复的概率是很小的

再写个void类型的start方法,start方法就是启动服务器,完成相应的业务逻辑,服务器的主要逻辑有三个:
- 读取请求并解析
- 根据请求计算响应
- 把响应写回到客户端中
这三个步骤应该写到while(true)中,因为服务器是一直开启的,等待客户端访问,代码逻辑如下:
- 读取请求需要调用socket的receive方法,receive方法中要传入一个DatagramPacket对象,把读取到的结果存储到数据报对象中的字节数组中(当没有接收到数据报时,receive就会阻塞等待,类似于阻塞队列)
因此要先创建个数据报对象reqPacket,铁汁们可以看下前面api中数据报对象的构造方法,就是传入一个字节数组,再传入数组的接收长度 - socket读取完成后,信息存到了reqPacket对象中的字节数组,要把这些信息转为字符串的形式,通过getData方法拿到reqPacket中的字节数组,用getLength获取数据包中的有效数据的长度,调用String方法的字节数组构造函数即可
- 把数据报信息转化成字符串的类型后,把字符串传到process方法中,process方法进行计算,返回要发送给客户端的字符串(这里因为是回显服务器,所以process中返回的还是原内容)
- 从process方法中接收到返回的字符串后,要把这个字符串再转化为数据报类型,创建个数据报对象respPacket(response),用于返回给客户端,用DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)这个构造方法,将字符串转化为字符数组
offset表示从哪个位置开始,传入0,length表示从 offset 位置开始,一共取多少个字节用来组成这个数据报,address就是目的主机的IP和端口号,就调用前面reqPacket对象的getSocketAddress方法,来获取主机IP和端口号数据(因为操作系统在解析IP报头和UDP报头时,会把信息存到数据报对象中) - 在末尾再写个打印日志,打印ip地址,端口号,以及请求字符串和返回的字符串
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//1.读取请求并解析
DatagramPacket reqPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(reqPacket);
String request = new String(reqPacket.getData(),0,reqPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端
DatagramPacket respPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,reqPacket.getSocketAddress());
socket.send(respPacket);
System.out.printf("[%s:%d] req:%s, resp:%s\n",reqPacket.getAddress(),reqPacket.getPort(),request,response);
}
}
public String process(String request){
return request;
}
2,客户端
首先写下成员变量和构造方法:
- 在客户端中还需要定义serveIp和servePort两个成员变量,因为客户端是主动发送数据的,要知道数据发送给谁,也就是要知道服务器的IP和端口号。而服务器发送数据的时候,就可以直接通过收到的请求,知道数据是要发送给谁
- 另外,创建socket对象时选用无参的构造方法,这样系统会为socket自动分配一个空闲的端口号,服务器不能这样搞,因为服务器必须有个确定的端口号,因为客户端访问时要根据IP和端口号选择选择访问哪个服务器
package network;
import java.net.DatagramSocket;
import java.net.SocketException;
public class EchoClient {
private DatagramSocket socket;
private String serveIp;
private String servePort;
public EchoClient(String serveIp, String servePort) throws SocketException {
this.serveIp = serveIp;
this.servePort = servePort;
socket = new DatagramSocket();
}
}
铁汁们思考个问题,如果在创建socket对象时,不让系统随机分配空闲端口号,而是自己传入一个固定的空闲端口号,可以吗?
这样做在自己计算机上测试测试客户端和服务器代码是没有一点问题的
但客户端代码最终是在用户的计算机上运行的,如果传入一个固定的端口号,这个端口号只是当前在程序员的计算机上是空闲的,在用户的计算机就不一定了,用户的计算机上会运行什么样的程序,占用什么样的端口,程序员是预知不到的,一旦这个端口已经在用户的计算机上被占用了,那客户端就运行失败,就属于是一个很严重的bug
start方法逻辑如下:
- 首先从控制台以字符串的形式读取用户输入的内容
- 再构建数据报,数据报中传入四个参数:字符串转成的字节数组、有效数据长度,目的IP、目的端口号,传目的IP的时候不能直接传serveIp,要转化为InetAddress对象,接着把内容发送给服务器
- 再构建个数据报respPacket,再用receive方法读取服务器的响应,把数据报转化为字符串,再打印字符串
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动!");
while(true){
//1.冲控制台中读取用户输入的内容
System.out.printf(">");
String request = scanner.next();
//2.构造数据报内容,里面包括发送的内容,目的IP和目的端口号
DatagramPacket reqPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serveIp),servePort);
socket.send(reqPacket);
//3.读取服务器的响应
DatagramPacket respPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(respPacket);
String response = new String(respPacket.getData(),0,respPacket.getLength());
//4.把响应显示到控制台
System.out.println(response);
}
}
3,测试
到这里服务器类和客户端类已经写完了,先在服务器类中写个main方法,启动一下服务器,启动后不要终止这个程序
public static void main(String[] args) throws IOException {
EchoServer server = new EchoServer(9090);
server.start();
}

再在客户端类中写个main方法,启动客户端,IP地址传入的是127.0.0.1,这个地址是回环地址,表示访问自己的主机,因为现在服务器和客户端都在一个主机上,所以这样写
写服务器和客户端代码,肯定要先在自己的机子上跑通代码,IP就是传入
127.0.0.1,表示客户端访问自己主机上的服务器
public static void main(String[] args) throws IOException {
EchoClient client = new EchoClient("127.0.0.1",9090);
client.start();
}

再到服务器控制台看下,在服务器的start方法中加了打印,每次处理时都会打印发送请求的客户端的IP、端口号、客户端发送的字符串、服务器返回的字符串

这里如果在IDEA上运行一个服务器,能不能让其他同学通过IP地址和服务器端口号来访问这个服务器,实现跨主机通信呢?
在同一个局域网中是可以的(也就是两个人的计算机连着同一个路由器的WiFi)
如果不在同一个局域网中,是访问不了的,因为我们计算机的IP是内网的私有IP,路由器外面互联网是不认这个地址的,想要实现真正的跨主机访问,就可以把服务器程序放到“云服务器上”(这部分涉及到程序打包、云服务器使用、防火墙,属于是JavaEE进阶的内容了,会在后面的博客中解析🐵)
4,UDP翻译服务器
写个简易的翻译服务器,客户端输入英语单词,服务器返回汉语翻译,服务器就叫做DictServer(dict是字典的意思)
除了对客户端请求的响应变了(process方法变了),其他逻辑跟回显服务器的逻辑基本上一样:创建socket、读取数据报、数据报转化字符串、响应后的字符串转化为数据报、数据报再发送回客户端
写的简单一点,就搞个HashMap,里面存储一些单词和汉语的键值对,当HashMap足够大时,就能完成单词的翻译,我们写代码时随便添加一些,意思一下即可:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class DictServer{
private DatagramSocket socket;
private Map<String,String> dict = new HashMap<>();
public DictServer(int port) throws SocketException {
socket = new DatagramSocket(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("pig", "小猪");
dict.put("bird", "小鸟");
dict.put("sheep", "小羊");
dict.put("cow", "小牛");
dict.put("chicken", "小鸡");
dict.put("rabbit", "小兔子");
dict.put("fish", "小鱼");
dict.put("tiger", "老虎");
dict.put("lion", "狮子");
dict.put("wolf", "狼");
dict.put("monkey", "猴子");
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//1.读取请求并解析
DatagramPacket reqPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(reqPacket);
String request = new String(reqPacket.getData(),0,reqPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端
DatagramPacket respPacket = new DatagramPacket(response.getBytes(),response.getBytes().length,reqPacket.getSocketAddress());
socket.send(respPacket);
System.out.printf("[%s:%d] req:%s, resp:%s\n",reqPacket.getAddress(),reqPacket.getPort(),request,response);
}
}
public String process(String request){
return dict.getOrDefault(request,"未找到该单词的翻译");
}
public static void main(String[] args) throws IOException {
DictServer server = new DictServer(9090);
server.start();
}
}
启动翻译服务器,直接用前面的回显客户端来连接服务器即可


结语💕💕
- 这里主要是练习服务器和客户端的交互,服务器的响应逻辑是比较简单的,实际服务器是比较复杂的,大型成熟服务器整体代码量能达到百万行级别,具体如何处理还是根据软件的具体需求
- 下篇博客会解析用TCP协议进行网络编程
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

🎆个人主页:
876



被折叠的 条评论
为什么被折叠?



