资讯详情

网络编程(一)UDP编程客户端/服务器代码详细解析

📅 2026/10/17 13:36:49 | 华诺云谱 👁 阅读
网络编程(一)UDP编程客户端/服务器代码详细解析
前言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 api2TCP和UDPTCP和UDP区别如下TCP有连接可靠传输面向字节流全双工UDP无连接不可靠传输面向数据报全双工有连接和无连接的理解可以类比结婚结婚的标志是两个人领了结婚证就相当于建立了连接两个人通过结婚证保存了对方的关键信息通过结婚证就能知道小帅的老婆是小美小美的老公是小帅网络上建立的是一种抽象的连接有无连接就是看双方是否保存了对方的关键信息TCP双方就会互相保存对方的信息UDP双方不会保存对方的信息如果需要保存还需要在应用层写代码来保存这个可靠/不可靠传输就跟丢包有关丢包是客观存在的可靠传输就是在对抗丢包TCP就是尽可能的确保数据能够被对方接收到属于可靠传输UDP则是把数据包发出去后就不再管了注一些文章中可能会把这个可靠性和安全性混淆了说TCP传输是安全的UDP传输是不安全的因为TCP尽可能确保数据能被对方接收到这个跟安全性没关系安全是指数据不容易被黑客截获/破解UDP传输的是数据报TCP传输的是字节流全双工指的是通信双方可以同时发送和接收数据收发互不干扰3UDP编程apiUDP编程首先要创建个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中接收指定长度第二个参数 lengthDatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个 DatagramPacket 以用来发送数据报发送的数据为字节数组第一个参数 buf中从 0 到指定长度第二个参数 length。address 指定目的主机的 IP 和端口号InetAddress getAddress()从接收的数据报中获取发送端主机 IP 地址或从发送的数据报中获取接收端主机 IP 地址int getPort()从接收的数据报中获取发送端主机的端口号或从发送的数据报中获取接收端主机端口号byte[] getData()获取数据报中的数据网络通信的基本流程如下图客户端主动发起通信服务器被动的接收通信通常是一个服务器要服务多个客户端的因为是基础部分一会写代码就先跑通一台服务器一个客户端的情况客户端和服务器要执行的任务和执行顺序如下图4UDP回显服务器/客户端1服务器我们就用UDP协议做个简单的“回显服务器”EchoServer回显服务器省略了服务器根据请求计算响应的部分把客户端发送的数据再原样返回给客户端首先写下服务器的代码定义个DatagramSocket的成员变量写个构造方法里面传入port也就是端口号创建DatagramSocket对象创建的时候传入端口号进行绑定此时这个socket就关联到了一个端口号端口号是上篇博客中提到的五元组中的一部分想发送信息就要知道目标端口号和源端口号例如服务器在启动时绑定一个端口号6060客户端在启动时绑定一个端口号9090客户端在给服务器发送请求时源端口是9090目标端口是6060服务器给客户端发送响应时源端口是6060目标端口是9090端口号在网络协议中是用两个字节表示的无符号整数范围就是0 - 655350 - 1024这个范围的端口号称为“知名端口号”系统预留给了一些知名协议的服务器来使用因此铁汁们绑定端口号的时候就不要绑定这个范围的packagenetwork;importjava.net.DatagramSocket;importjava.net.SocketException;publicclassEchoServer{privateDatagramSocketsocket;publicEchoServer(intport)throwsSocketException{socketnewDatagramSocket(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表示从哪个位置开始传入0length表示从 offset 位置开始一共取多少个字节用来组成这个数据报address就是目的主机的IP和端口号就调用前面reqPacket对象的getSocketAddress方法来获取主机IP和端口号数据因为操作系统在解析IP报头和UDP报头时会把信息存到数据报对象中在末尾再写个打印日志打印ip地址端口号以及请求字符串和返回的字符串publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//1.读取请求并解析DatagramPacketreqPacketnewDatagramPacket(newbyte[4096],4096);socket.receive(reqPacket);StringrequestnewString(reqPacket.getData(),0,reqPacket.getLength());//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回到客户端DatagramPacketrespPacketnewDatagramPacket(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);}}publicStringprocess(Stringrequest){returnrequest;}2客户端首先写下成员变量和构造方法在客户端中还需要定义serveIp和servePort两个成员变量因为客户端是主动发送数据的要知道数据发送给谁也就是要知道服务器的IP和端口号。而服务器发送数据的时候就可以直接通过收到的请求知道数据是要发送给谁另外创建socket对象时选用无参的构造方法这样系统会为socket自动分配一个空闲的端口号服务器不能这样搞因为服务器必须有个确定的端口号因为客户端访问时要根据IP和端口号选择选择访问哪个服务器packagenetwork;importjava.net.DatagramSocket;importjava.net.SocketException;publicclassEchoClient{privateDatagramSocketsocket;privateStringserveIp;privateStringservePort;publicEchoClient(StringserveIp,StringservePort)throwsSocketException{this.serveIpserveIp;this.servePortservePort;socketnewDatagramSocket();}}铁汁们思考个问题如果在创建socket对象时不让系统随机分配空闲端口号而是自己传入一个固定的空闲端口号可以吗这样做在自己计算机上测试测试客户端和服务器代码是没有一点问题的但客户端代码最终是在用户的计算机上运行的如果传入一个固定的端口号这个端口号只是当前在程序员的计算机上是空闲的在用户的计算机就不一定了用户的计算机上会运行什么样的程序占用什么样的端口程序员是预知不到的一旦这个端口已经在用户的计算机上被占用了那客户端就运行失败就属于是一个很严重的bugstart方法逻辑如下首先从控制台以字符串的形式读取用户输入的内容再构建数据报数据报中传入四个参数字符串转成的字节数组、有效数据长度目的IP、目的端口号传目的IP的时候不能直接传serveIp要转化为InetAddress对象接着把内容发送给服务器再构建个数据报respPacket再用receive方法读取服务器的响应把数据报转化为字符串再打印字符串publicvoidstart()throwsIOException{ScannerscannernewScanner(System.in);System.out.println(客户端启动);while(true){//1.冲控制台中读取用户输入的内容System.out.printf();Stringrequestscanner.next();//2.构造数据报内容里面包括发送的内容目的IP和目的端口号DatagramPacketreqPacketnewDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serveIp),servePort);socket.send(reqPacket);//3.读取服务器的响应DatagramPacketrespPacketnewDatagramPacket(newbyte[4096],4096);socket.receive(respPacket);StringresponsenewString(respPacket.getData(),0,respPacket.getLength());//4.把响应显示到控制台System.out.println(response);}}3测试到这里服务器类和客户端类已经写完了先在服务器类中写个main方法启动一下服务器启动后不要终止这个程序publicstaticvoidmain(String[]args)throwsIOException{EchoServerservernewEchoServer(9090);server.start();}再在客户端类中写个main方法启动客户端IP地址传入的是127.0.0.1这个地址是回环地址表示访问自己的主机因为现在服务器和客户端都在一个主机上所以这样写写服务器和客户端代码肯定要先在自己的机子上跑通代码IP就是传入127.0.0.1表示客户端访问自己主机上的服务器publicstaticvoidmain(String[]args)throwsIOException{EchoClientclientnewEchoClient(127.0.0.1,9090);client.start();}再到服务器控制台看下在服务器的start方法中加了打印每次处理时都会打印发送请求的客户端的IP、端口号、客户端发送的字符串、服务器返回的字符串这里如果在IDEA上运行一个服务器能不能让其他同学通过IP地址和服务器端口号来访问这个服务器实现跨主机通信呢在同一个局域网中是可以的也就是两个人的计算机连着同一个路由器的WiFi如果不在同一个局域网中是访问不了的因为我们计算机的IP是内网的私有IP路由器外面互联网是不认这个地址的想要实现真正的跨主机访问就可以把服务器程序放到“云服务器上”这部分涉及到程序打包、云服务器使用、防火墙属于是JavaEE进阶的内容了会在后面的博客中解析4UDP翻译服务器写个简易的翻译服务器客户端输入英语单词服务器返回汉语翻译服务器就叫做DictServerdict是字典的意思除了对客户端请求的响应变了(process方法变了)其他逻辑跟回显服务器的逻辑基本上一样创建socket、读取数据报、数据报转化字符串、响应后的字符串转化为数据报、数据报再发送回客户端写的简单一点就搞个HashMap里面存储一些单词和汉语的键值对当HashMap足够大时就能完成单词的翻译我们写代码时随便添加一些意思一下即可packagenetwork;importjava.io.IOException;importjava.net.DatagramPacket;importjava.net.DatagramSocket;importjava.net.SocketException;importjava.util.HashMap;importjava.util.Map;publicclassDictServer{privateDatagramSocketsocket;privateMapString,StringdictnewHashMap();publicDictServer(intport)throwsSocketException{socketnewDatagramSocket(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,猴子);}publicvoidstart()throwsIOException{System.out.println(服务器启动);while(true){//1.读取请求并解析DatagramPacketreqPacketnewDatagramPacket(newbyte[4096],4096);socket.receive(reqPacket);StringrequestnewString(reqPacket.getData(),0,reqPacket.getLength());//2.根据请求计算响应Stringresponseprocess(request);//3.把响应写回到客户端DatagramPacketrespPacketnewDatagramPacket(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);}}publicStringprocess(Stringrequest){returndict.getOrDefault(request,未找到该单词的翻译);}publicstaticvoidmain(String[]args)throwsIOException{DictServerservernewDictServer(9090);server.start();}}启动翻译服务器直接用前面的回显客户端来连接服务器即可结语这里主要是练习服务器和客户端的交互服务器的响应逻辑是比较简单的实际服务器是比较复杂的大型成熟服务器整体代码量能达到百万行级别具体如何处理还是根据软件的具体需求下篇博客会解析用TCP协议进行网络编程以上就是今天的所有内容啦完结撒花
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑