各位读者好, 我是小陈, 这是我的个人主页
小陈还在持续努力学习编程, 努力通过博客输出所学知识
如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
希望我的专栏能够帮助到你:
JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 堆, 二叉树, 二叉搜索树, 哈希表等
JavaEE初阶: 多线程, 网络编程, TCP/IP协议, HTTP协议, Tomcat, Servlet, Linux, JVM等(正在持续更新)
上篇文章介绍了网络原理中 TCP/IP 五层网络模型, 以及数据在网络上使如何传输的基本知识
本篇将介绍网络编程中 : 基于 UDP 协议的 Socket 套接字的相关知识
提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!
上篇提到, 我们程序员进行网络编程主要是在 TCP/UDP 五层网络模型中的应用层, 而数据在网络上传输, 需要进行封装和分用, 其中应用层需要调用传输层提供的 API , 这一组 API 就被称作 Socket API
概念 : Socket 套接字是由系统提供于网络通信的技术, 是基于 TCP/IP 协议的网络通信的基本操作党员, 基于 Socket 套接字的网络程序开发就是网络编程
要进行网络通信, 需要有一个 socket 对象, 一个 socket 对象对应着一个 socket 文件, 这个文件在 网卡上而不是硬盘上, 所以有了 sokcet 对象才能通过操作内存来操作网卡
在 socket 文件中写数据相当于通过网卡发送数据, 在 socket 文件中读数据相当于通过网卡接收数据
Socket API 分为两类 : 基于 TCP 协议的 API , 和基于 UDP 协议的 API, 下面先认识一下 TCP 协议和 UDP 协议的区别和特点
TCP 协议 | 说明 | UDP 协议 | 说明 |
---|---|---|---|
有连接 | 通信双方需要刻意保存对方的相关信息 | 无链接 | 通信双方不需要刻意保存对方的信息 |
可靠传输 | 如果数据发送不成功, 发送方会知道 | 不可靠传输 | 发送方不关心数据是否发送成功 |
面向字节流 | 发送的数据以字节为单位 | 面向数据报 | 发送的数据以 UDP 数据报为单位 |
全双工 | 双向通信 | 全双工 | 双向通信 |
这里只做简单介绍, 这两个协议后续会单独详细介绍
UDP 的 Sokcet API 相对更简单易学, 本文先介绍这部分内容, TCP 的 Sokcet API 下篇文章介绍
❗️❗️❗️基于 UDP 协议的 Socket API 中, 要分清楚以下两个类 :
类名 | 解释 |
---|---|
DatagramSocket | 这个类表示一个 Socket, 用于发送和接收 UDP 数据报 |
DatagramPacket | 这个类表示一个 UDP 数据报 |
这个两个类的关系就相当于 : DatagramSocket 是取餐和送餐的外卖小哥, 而 DatagramSocket 就是外卖 (Datagram 就是数据报的意思, packet 是小包裹的意思)
记清楚啊 ! 别搞混了 ! ! 下面分别介绍这两个类的构造方法和成员方法, 待会写代码要用
DatagramSocket 类的构造方法 :
方法签名 | 作用 |
---|---|
DatagramSocket() | 创建一个 UDP Socket 对象, 一般用于客户端, 不需要指定本机端口号, 由操作系统随机分配 |
DatagramSocket (int port) | 创建一个 UDP Socket 对象, 一般用于服务器, 需要指定本机端口号(port) |
DatagramSocket 类的成员方法 :
方法签名 | 作用 |
---|---|
void receive(DatagramPacket p) | 接收数据报, 如果没接收到, 阻塞等待 |
void send(DatagramPacket p) | 发送数据报, 不会阻塞等待 |
void close() | 关闭套接字 |
表示 Socket 的这个类小总结 :
有两个构造方法, 一个是给服务器提供的, 一个是给客户端提供的
有三个成员方法, 分别是接收数据报, 发送数据报, 关闭套接字
DatagramPacket 类的构造方法 :
方法签名 | 作用 |
---|---|
DatagramPacket(byte buf[], int length) | 创建一个数据报对象, 用于接收数据报, 接收到的数据存在 buf (第一个参数: 字节数组) 中, 需要指定 length (第二个参数: 长度) , |
DatagramPacket(byte buf[], int length, InetAddress address, int port | 创建一个数据报对象, 用于发送数据报, 发送的数据存在 buf (第一个参数: 字节数组) 中, 需要指定 length (第二个参数: 长度) , address (第三个参数: 主机 IP 地址) , port (第四个参数: 主机端口号) |
DatagramPacket 类的成员方法 :
方法签名 | 作用 |
---|---|
InetAddress getAddress() | 从接收或发送的数据报中获取对端的 IP 地址 |
int getPort() | 从接收或发送的数据报中获取对端的端口号 |
byte[] getData() | 获取数据报中的数据, 数据存在字节数组中 |
int getLength() | 获取数据报中的数据的实际长度 |
表示 Packet 的这个类的小总结 :
有两个构造方法, 一个用于接收数据包, 一个用于发送数据报
有四个成员方法, 分别是获取 IP 地址, 获取端口号, 获取数据, 获取数据长度
看到这可能有点蒙了, 这都是啥啊, 太多了太乱了
别急, 有个大概的印象即可, 接下来逐行解析如何从 0 到 1 地进行客户端和服务器之间地网络编程, 代码敲完之后再消化吸收
下面我们写一个最简单的客户端服务器网络通信模型 : 客户端给服务器发送什么请求, 服务器就给客户发送什么响应(这是最简单但是毫无意义的回显服务器, 只是方便熟悉 UDP Socket 的 API 使用)
客户端和服务器各自为一个进程在运行, 双方互不干涉(当然我们现在要写的客户端服务器程序是在同一台主机上的)
一定是服务器先启动, 一直等待客户端发来请求, 所以按照时间顺序, 代码逻辑应该如下所示 :
客户端 | 服务器 |
---|---|
/ | 1,启动服务器, 阻塞等待, 时刻准备接收客户端发来的请求 |
2, 发送请求 | / |
/ | 3,接收请求 |
/ | 4,处理请求 |
/ | 5,返回响应给客户端 |
6, 接收响应 | / |
有了这个思路, 下面正式开始使用上述 API 进行网络编程
创建一个类 UDPEchoClient 作为客户端
成员属性 :
1️⃣首先定义一个 Scoket 对象来接收和发送数据报
2️⃣客户端发送的请求数据报, 需要指定服务器的 IP 地址和端口号, 所以需要定义 serverIP 和 serverPort
public class UDPEchoClient {
private DatagramSocket socket = null;// 1,socket对象
private String serverIP = null;// 2,服务器IP地址
private int serverPort = 0;// 3,服务器端口号
}
构造方法 :
在构造方法中对上述三个成员属性进行初始化
public class UDPEchoClient {
// 成员属性
private DatagramSocket socket = null;// 1,socket对象
private String serverIP = null;// 2,服务器IP地址
private int serverPort = 0;// 3,服务器端口号
// 构造方法
public UDPEchoClient(String serverIP, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
}
main 方法 :
1️⃣构造 udpEchoClient 对象, 由于服务器在本机, IP 地址为"127.0.0.1", 端口号随意指定 [1024, 65535] 之间的任意一个
2️⃣调用 UDPEchoClient 类的核心成员方法 start(), 这个方法实现了客户端的核心逻辑
public class UDPEchoClient {
// 成员属性
private DatagramSocket socket = null;// 1,socket对象
private String serverIP = null;// 2,服务器IP地址
private int serverPort = 0;// 3,服务器端口号
// 构造方法
public UDPEchoClient(String serverIP, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
// main 方法
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 9999);
udpEchoClient.start();
}
}
1️⃣构造一个 Scanner 对象, 从控制台输入字符串, 这个字符串当作请求的内容
2️⃣核心逻辑在一个 while(true) 循环中, 实现多次发送请求
public void start() throws IOException {
Scanner in = new Scanner(System.in);
while(true) {
}
}
⚠️⚠️⚠️注意 :
上述代码中, 要分清 : socket.send(DatagramPacket p)用于发送数据报, socket.receive(DatagramPacket p)用于接收数据报,
而参数都是 DatagramPacket 类型, 所以在发送和接收之前, 需要 new 出来数据报对象呀, 但是用于发送和接受的数据报的构造方法是不同的, 请结合上图中黄色部分再做区分
方法签名 | 作用 |
---|---|
DatagramPacket(byte buf[], int length) | 创建一个数据报对象, 用于接收数据报, 接收到的数据存在 buf (第一个参数: 字节数组) 中, 需要指定 length (第二个参数: 长度) , |
DatagramPacket(byte buf[], int length, InetAddress address, int port | 创建一个数据报对象, 用于发送数据报, 发送的数据存在 buf (第一个参数: 字节数组) 中, 需要指定 length (第二个参数: 长度) , address (第三个参数: 主机 IP 地址) , port (第四个参数: 主机端口号) |
另外, start()中第三步相当于是把数据报中的数据(btye[])解析成字符串, 方便我们观察
创建一个类 UDPEchoServer 作为服务器
成员属性 :
服务器不需要指定客户端的 IP 地址和端口号, 所以构造方法只需要定义一个 socket 对象即可
public class UDPEchoServer {
// 成员属性
private DatagramSocket socket = null;// socket 对象
}
构造方法 :
用于实例化服务器的 socket 对象, 别忘了服务器使用的 socket 需要绑定本机的端口号
public class UDPEchoServer {
// 成员属性
private DatagramSocket socket = null;// socket 对象
// 构造方法
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
}
main 方法 :
1️⃣用于实例化 UDPEchoServer 对象, 端口号为刚才客户端指定的端口号
2️⃣调用 UDPEchoServer 类的核心成员方法 start(), 这个方法实现了服务器的核心逻辑
public class UDPEchoServer {
// 成员属性
private DatagramSocket socket = null;// socket 对象
// 构造方法
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// main方法
public static void main(String[] args) throws IOException {
UDPEchoServer server = new UDPEchoServer(9999);
server.start();
}
}
服务器启动之后, 等待客户端发来消息, 实际上一台服务器会服务多个客户端, 所以核心代码写在 while(true) 循环中, 这样才能够处理完一个客户端发来的请求之后继续处理
public void start() throws IOException {
while(true) {
}
}
图解如下 :
写完客户端之后再写服务器就相对熟悉了, 都是一个套路 : 在发送或者接收数据报之前, 要先 new 出 DatagramPacket 对象, 只是需要注意构造方法的使用
需要补充的是, 在第一步中, 发送完数据报之后, 把数据报解析成了字符串方便处理(并不一定所有的服务器程序都需要这么做, 在当前这个程序下处理成字符串更方便而已)
而 process 方法用于处理请求, 真实的服务器的这一步会根据业务逻辑把代码写的非常完善, 而我们实现的仅仅是最简单的回显服务器, 所以我们的 process 方法只需要 return 即可
客户端运行效果截图 : 注意一定要先运行服务器
客户端输出的是请求(字符串) + 响应(字符串)
服务器运行效果截图
服务器输出的是本机IP地址, 客户端端口号(系统自动分配的), 请求(String), 响应(String)
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UDPEchoClient {
// 成员属性: 1,socket对象 2,服务器IP地址 3,服务器端口号
private DatagramSocket socket = null;
private String serverIP = null;
private int serverPort = 0;
// 构造方法
public UDPEchoClient(String serverIP, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
Scanner in = new Scanner(System.in);
while(true) {
// 1,从控制台输入数据, 构造成 DatagramPacket , 把这个请求发给服务器
String requestString = in.next();
DatagramPacket requestPacket = new DatagramPacket(requestString.getBytes()
, requestString.getBytes().length, InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 2,接收服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[9999], 9999);
socket.receive(responsePacket);
// 3,转换成字符串
String responseString = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 打印客户端请求 + 服务器响应
System.out.println(requestString + " + " + responseString);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 9999);
udpEchoClient.start();
}
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UDPEchoServer {
// 成员属性: 定义一个Socket对象
private DatagramSocket socket = null;
// 构造方法
public UDPEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
while(true) {
// 1,从客户端读取请求数据
DatagramPacket requestPacket = new DatagramPacket(new byte[9999], 9999);
socket.receive(requestPacket);
String requestString = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2,处理请求
String responseString = process(requestString);
// 3,把响应发送给客户端
DatagramPacket responsePacket = new DatagramPacket(responseString.getBytes()
, responseString.getBytes().length, requestPacket.getAddress(), requestPacket.getPort());
socket.send(responsePacket);
// 打印客户端IP地址 + 客户端端口号 + 客户端请求服务器相应
System.out.println(requestPacket.getAddress().toString() + " + " + requestPacket.getPort() + " + "
+ requestString + " + " + responseString);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UDPEchoServer server = new UDPEchoServer(9999);
server.start();
}
}
以上就是本篇的全部内容, 主要介绍了 : 基于 UDP 协议的 Socket API , 以及利用这些 API 写了一个最简单但无意义的客户端服务器网络通信程序
有一点需要说明 : 这个程序中没有调用 sockt.close() 是因为这客户端服务器的生命周期很长, 并没有频繁的销毁和创建,
如果我们的客户端和服务器停止运行了, 那么我们的main方法也结束了, 整个 Java 进程就结束了, 所以没必要显式的调用 close
方法
再回顾一下, 基于 UDP 协议的 Socket API 主要有两个类:
表示 Socket 的这个类, DatagramSocket :
有两个构造方法, 一个是给服务器提供的, 一个是给客户端提供的
有三个成员方法, 分别是接收数据报, 发送数据报, 关闭套接字
表示 Packet 的这个类, DatagramPacket :
有两个构造方法, 一个用于接收数据包, 一个用于发送数据报
有四个成员方法, 分别是获取 IP 地址, 获取端口号, 获取数据, 获取数据长度
下篇分享基于 TCP 协议的 Socket API, 关于 TCP 和 UDP 协议会在后续的文章详细介绍
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦~
上山总比下山辛苦
下篇文章见