WebSocket网络编程(TCP/UDP)
网络编程
一、UDP-API(JDK)
1、DatagramSocket

Java java.net
public class DatagramSocket implements java.io.Closeable
此类表示用于发送和接收数据报包的套接字。
数据报套接字是数据包投递服务的发送或接收端点。通过数据报套接字发送或接收的每个数据包都具有独立的地址和路由策略。从一台机器发送到另一台机器的多个数据包可能采用不同路由路径,且可能以任意顺序到达。
在实现允许的情况下,新构建的 `DatagramSocket` 会默认启用 `SO_BROADCAST` 套接字选项,以支持广播数据报的传输。若要接收广播数据包,应将 DatagramSocket 绑定到通配符地址。在某些实现中,当 DatagramSocket 绑定到特定地址时也可能收到广播数据包。
示例:DatagramSocket s = new DatagramSocket(null); s.bind(new InetSocketAddress(8888)); 等效于:DatagramSocket s = new DatagramSocket(8888); 两种方式都会创建能够接收 UDP 端口 8888 广播的 DatagramSocket。
起始版本:JDK1.0
另请参阅:DatagramPacket, |
1.1、DatagramSocket(int port)

Plain Text public DatagramSocket( int port ) throws java.net.SocketException
构建数据报套接字并将其绑定到本地主机的指定端口。该套接字将绑定到通配符地址(由内核选择的IP地址)。
若存在安全管理器,则会首先调用其 checkListen 方法并以端口参数作为参数进行检查,以确保操作被允许。这可能引发 SecurityException。
参数: port – 要使用的端口。
抛出: SocketException – 如果无法打开套接字,或套接字无法绑定到指定本地端口。 SecurityException – 如果存在安全管理器且其 checkListen 方法不允许该操作。
另请参阅: SecurityManager.checkListen |
1.2、DatagramPacket(byte[] buf,int length)
Java // 1. 读取请求并解析. 此处 requestPacket 是 receive 的输出型参数. DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024); |

Java java.net.DatagramPacket
@Contract(pure = true) public DatagramPacket( @NotNull byte[] buf, int length )
构造用于接收指定长度数据报包的DatagramPacket。
length参数必须小于或等于buf.length的长度。
参数:buf - 用于保存传入数据报的缓冲区。 length - 需要读取的字节数。
< JDK 1.8 > |
2、DatagramPacket

Java java.net public final class DatagramPacket
该类表示一个数据报包(datagram packet)。
数据报包用于实现无连接的数据包投递服务。每条消息仅根据包内包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个数据包可能采用不同的路由路径,且可能以任意顺序到达。数据包投递不保证可靠性。
起始版本:JDK1.0
作者:Pavani Diwanji, Benjamin Renaud
< JDK 1.8 > (rt.jar) |
1.1 public DatagramPacket( byte[] buf, int length, java.net.SocketAddress address)

Java @contract(value = "_,null->fail", pure = true) public DatagramPacket( @NotNull byte[] buf, int length, java.net.SocketAddress address )
构造用于向指定主机的指定端口发送长度为length的数据报包的DatagramPacket。length参数必须小于或等于buf.length的长度。
参数:buf - 数据包内容 length - 数据包长度 address - 目标地址
抛出:IllegalArgumentException - 如果地址类型不受支持
起始版本:1.4
另请参阅:InetAddress
< JDK 1.8 > |
3、ScoketAddress

Java java.net public abstract class SocketAddress implements java.io.Serializable 说明: 这个类表示一个 没有绑定任何协议的套接字地址。 作为一个抽象类,它需要由某个 特定的、依赖协议的实现类 来继承。 它提供了一个 不可变对象,被套接字用于 绑定、连接 或作为返回值使用。 自版本:1.4 参见:Socket,ServerSocket |
方法一、requestPacket.getData()

方法二,字符串构造方法(新)

Java java.lang.String @Contract(pure = true) public String( @NotNull byte[] bytes, int offset, int length )
通过使用平台的默认字符集解码指定的字节子数组来构造一个新的`String`。新`String`的长度是字符集的函数,因此不一定等于子数组的长度。
当给定字节在默认字符集中无效时,此构造方法的行为是未指定的。若需要对解码过程进行更多控制,应使用`java.nio.charset.CharsetDecoder`类。
参数:bytes - 要解码为字符的字节数组 offset - 要解码的第一个字节的索引 length - 要解码的字节数
抛出:IndexOutOfBoundsException - 如果`offset`和length参数索引超出bytes数组的边界
起始版本:JDK1.1int length )
// 此处把二进制数据, 转成字符串. String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
|
方法三、requestPacket.getSocketAddress()

Java 说明 获取数据报包要发送到的远程主机,或者数据报包来自的远程主机的 SocketAddress(通常是 IP 地址 + 端口号)。 返回值 返回这个 SocketAddress。 自版本 1.4 起引入。 另请参见 setSocketAddress |
3.1 requestPacket.getAddress()
3.2 requestPacket.getPort()
方法四、socket.send(responsePacket)

Java java.net.DatagramSocket public void send( java.net.DatagramPacket p )throws java.io.IOException
方法说明 从该套接字发送一个数据报包。 DatagramPacket 包含的信息包括:要发送的数据、数据长度、远程主机的 IP 地址,以及远程主机的端口号。 如果存在一个安全管理器,并且套接字当前未连接到远程地址,那么此方法会首先执行一些安全检查: 首先,如果 p.getAddress().isMulticastAddress() 为 true, 则此方法会调用安全管理器的 checkMulticast 方法,并将 p.getAddress() 作为参数传入。 如果上述条件的判断结果为 false,那么此方法会改为调用安全管理器的 checkConnect 方法, 传入的参数是 p.getAddress().getHostAddress() 和 p.getPort()。 对安全管理器方法的每一次调用,如果操作不被允许,都可能导致抛出 SecurityException。
参数 p —— 要被发送的 DatagramPacket。
抛出异常 IOException —— 如果发生 I/O 错误。 SecurityException |
方法五、socket.receive()

Java java.net.DatagramSocket public void receive( java.net.DatagramPacket p )throws java.io.IOException
方法说明 从该套接字接收一个数据报包。当此方法返回时,DatagramPacket 的缓冲区会被接收到的数据填充。 数据报包同时包含发送方的 IP 地址 和发送方机器上的 端口号。 此方法会阻塞,直到有数据报被接收。数据报包对象中的 length 字段包含接收到消息的长度。 如果消息的长度大于包的长度,消息将被截断。 如果存在安全管理器,那么当安全管理器的 checkAccept 方法不允许接收时,该数据报包无法被接收。
参数 p —— DatagramPacket 对象,用来存放接收到的数据。
抛出异常 IOException —— 如果发生 I/O 错误。 SocketTimeoutException —— 如果之前调用过 setSoTimeout,并且超时时间已过。 PortUnreachableException —— 当套接字已连接到某个当前不可达的目标地址时,可能抛出此异常。 |
UDP 通信是无连接的,角色上不再区分严格的“服务端”和“客户端”,而是更偏向于发送端和接收端。任何一个 DatagramSocket 既可以发送也可以接收数据报。
二、TCP-API(JDK)
1、ServerSocket

Java java.net public class ServerSocket implements java.io.Closeable
该类实现了服务器套接字。服务器套接字等待通过网络传入的请求,基于该请求执行某些操作,然后可能向请求者返回结果。
服务器套接字的实际工作由 `SocketImpl` 类的实例执行。应用程序可以通过更改创建套接字实现的套接字工厂,来配置自身创建适用于本地防火墙的套接字。
自:JDK1.0 起
另请参阅:SocketImpl, setSocketFactory(SocketImplFactory), ServerSocketChannel
作者:未署名
< 1.8 > (t[ja]) |
每次服务器收到一个客户端的连接,都会通过ServerSocket进行accept
(ServerSocket只需要进行accept即可,不提供具体的读写操作)
1.1 accept
Java Socket socket = serverSocket.accept(); |

2.Socket

Java java.net public class Socket implements java.io.Closeable
该类实现了客户端套接字(也简称为"套接字")。套接字是两台机器之间通信的端点。
套接字的实际工作由 `SocketImpl` 类的实例执行。应用程序可以通过更改创建套接字实现的套接字工厂,来配置自身创建适用于本地防火墙的套接字。
自:JDK1.0 起
另请参阅:setSocketImplFactory(SocketImplFactory), SocketImpl, SocketChannel. 作者:未署名
< 1.8 > (t|jar) |
TCP是面向字节流的,不同于UDP并没有数据报这种数据包,而是利用流传输进行字节的传输
一、UDP-API
一、通信端点核心类:DatagramSocket
这个类表示用于发送和接收数据报包的套接字。它是 UDP 通信的端点。
- DatagramSocket()
- 作用:创建一个未绑定特定端口的套接字。通常用于客户端或发送端,系统会为其分配一个随机的可用端口。
- 注意:创建后可以立即用于发送数据报。如需接收数据报,则需要知道系统分配的随机端口号,因此接收端更常用下面的构造方法。
- DatagramSocket(int port)
- 作用:创建一个绑定到特定端口的套接字。这是接收端最常用的构造函数。
- 关键点:一个端口只能被一个进程绑定。用于接收的套接字必须绑定到一个已知的、固定的端口。
- DatagramSocket(int port, InetAddress laddr)
- 作用:创建一个套接字,绑定到指定的本地地址和端口。用于有多块网卡的机器,可以指定在哪块网卡的 IP 地址上监听。
- void send(DatagramPacket p)
- 作用:发送数据报包。从此套接字发送指定的数据报包 p。包中包含了目标地址和端口信息。
- 参数:p - 要发送的 DatagramPacket。
- void receive(DatagramPacket p)
- 作用:接收数据报包。此方法会阻塞,直到收到一个数据报。接收到的数据将被填充到参数包 p 的缓冲区中,同时包的源地址和端口会被设置为发送方的地址和端口。
- 参数:p - 一个用于接收数据的 DatagramPacket(需要预先分配好缓冲区)。
- 关键点:这是一个阻塞调用,通常放在一个循环中。参数 p 是一个“空容器”,receive 方法会把它“装满”。
- void setSoTimeout(int timeout)
- 作用:设置 receive() 方法的超时时间(毫秒)。超时后会抛出 SocketTimeoutException,允许线程从阻塞中退出,检查其他状态(如是否该关闭)。非常重要,否则程序可能永远阻塞。
- void close()
二、数据容器核心类:DatagramPacket
这个类代表一个数据报包。它就像是一个“信封”,里面包含了要发送的数据和目标的地址/端口,或者接收到的数据和源的地址/端口。
- DatagramPacket(byte[] buf, int length)
- length - 要读取的字节数(必须 <= buf.length)。
- DatagramPacket(byte[] buf, int length, InetAddress address, int port)
- 作用:构造一个用于发送数据报的包。它指定了目标地址和端口。
- length - 要发送的字节数(必须 <= buf.length)。
- address - 目标主机地址(InetAddress)。
- InetAddress getAddress()
- 对于接收包:返回的是发送方的源地址。极其重要,用于获取谁发来了数据,以便回复。
- int getPort()
- byte[] getData()
- 作用:返回数据缓冲区。通常与 getLength() 和 getOffset() 一起使用,来获取接收到的有效数据。
- 注意:返回的是整个初始缓冲区,而不仅仅是有效数据部分。需要用 getLength() 来确定实际收到了多少数据。
- int getLength()
二、TCP-API
TCP 通信是面向连接的,分为服务端 (ServerSocket) 和客户端 (Socket) 两种角色。
一、服务端核心类:ServerSocket
这个类用于在服务器上监听特定端口,等待客户端的连接请求。
- ServerSocket(int port)
- 作用:创建一个绑定到特定端口的服务器套接字。这是最常用的构造函数。
- 注意:如果端口号为 0,则系统会随机分配一个可用端口(称为匿名端口),通常用于客户端,服务端较少用。
- Socket accept()
- 作用:这是最核心的方法。它监听并接受客户端的连接请求。该方法会阻塞,直到有客户端连接进来,然后返回一个新的 Socket 对象用于与该客户端通信。
- 返回值:一个代表与客户端建立的连接的 Socket 对象。
- 关键点:服务端通过一个 ServerSocket 接收多个客户端连接,通常会用循环调用 accept() 方法,并为每个返回的 Socket 启动一个新线程来处理,以实现并发。
- void setSoTimeout(int timeout)
- 作用:设置 accept() 方法的超时时间(毫秒)。设置后,accept() 在指定时间内没有连接请求则会抛出 SocketTimeoutException,之后可以继续调用 accept()。
- 为何重要:避免了服务端程序永远阻塞在 accept() 上,让程序有机会检查其他状态(如是否该关闭服务)。
- void close()
- 作用:关闭此服务器套接字,释放端口资源。继承自 java.io.Closeable,通常用在 try-with-resources 或 finally 块中。
二、客户端 & 连接核心类:Socket
这个类代表一个已经建立的 TCP 连接的两端,无论是客户端还是服务端处理连接的线程,都使用这个类的对象进行通信。
- Socket(String host, int port)
- 作用(客户端):向指定的服务器主机和端口发起连接请求。这是客户端最常用的构造函数。
- InputStream getInputStream()
- 作用:获取从此套接字读取数据的输入流。通过读取这个流,可以接收到对方发送过来的数据。
- 如何使用:通常会包装成 BufferedReader(用于文本)或 DataInputStream(用于基本数据类型)。
- OutputStream getOutputStream()
- 作用:获取向此套接字写入数据的输出流。通过向这个流写入数据,可以将数据发送给连接的另一方。
- 如何使用:通常会包装成 BufferedWriter / PrintWriter(用于文本)或 DataOutputStream(用于基本数据类型)。
- 关键点:getInputStream() 和 getOutputStream() 是 TCP 通信数据传输的基石。
- void shutdownOutput()
- 作用:禁用此套接字的输出流。发送完数据后调用此方法,会向对方发送一个 EOF(文件结束符)。
- 为何重要:这是一种优雅的关闭方式,告诉对方“我的数据已经发完了”,但连接还可以继续用来接收数据。对于解决读阻塞问题至关重要(例如,客户端发送完数据后通知服务端,服务端的读操作才能结束)。
- void close()
- 作用:关闭此套接字,并释放关联的 I/O 流等所有资源。同样,应在 finally 块或使用 try-with-resources 确保关闭。
三、UDP服务端server代码
Java package test1;
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException;
public class UdpEchoServer { private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); }
// 启动服务器. public void start() throws IOException { System.out.println("server start!");
while (true) { // 1. 读取请求并解析. 此处 requestPacket 是 receive 的输出型参数. DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024); socket.receive(requestPacket); // 此处把二进制数据, 转成字符串. String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); // 2. 根据请求计算响应. (这里通常是一个复杂的过程, 但是由于此处 回显服务器, 没有计算的过程) String response = process(request); // 3. 把响应返回给客户端. 再构造一个 DatagramPacket 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); } }
// 根据请求计算响应. // 由于当前是回显服务器, 直接把 request 作为 response 返回了. // 未来编写其他服务器, 只需要把 process 里的逻辑进行调整即可. private String process(String request) { return request; }
public static void main(String[] args) throws IOException { // 如何知道这个端口号和别人不重复?? // 如果重复了, 就会报错~~ UdpEchoServer server = new UdpEchoServer(9090); server.start(); } } |
四、UDP客户端client代码
Java package test1;
import java.io.IOException; import java.net.*; import java.util.Scanner;
public class UdpEchoClient { // 创建 socket 对象 private DatagramSocket socket = null; private String serverIp; private int serverPort;
public UdpEchoClient(String serverIp, int serverPort) throws SocketException { socket = new DatagramSocket(); this.serverIp = serverIp; this.serverPort = serverPort; }
public void start() throws IOException { System.out.println("client start!");
Scanner scanner = new Scanner(System.in);
// 用户通过控制台, 输入字符串, 把字符串发给服务器. 从服务器读取响应. while (true) { // 1. 从控制台读取用户输入. System.out.print("-> "); String request = scanner.next(); if (request.equals("exit")) { break; } // 2. 把用户输入的字符串构造成 UDP 数据报, 进行发送. DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIp), this.serverPort); socket.send(requestPacket); // 3. 从服务器读取响应 DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024); socket.receive(responsePacket); String response = new String(responsePacket.getData(), 0, responsePacket.getLength()); // 4. 显示响应. System.out.println(response); } }
public static void main(String[] args) throws IOException { UdpEchoClient client = new UdpEchoClient("101.200.34.238", 9090); client.start(); } } |
五、客户端/服务器实现代码浅解
通信五元组

在UdpEchoServer服务器的构造方法中,我们通过为构造方法添加了int port参数,来确定了服务器程序的端口号
Java public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); } |
而在UdpEchoClient客户端的构造方法中,我们却并没设置int port端口号参数,而是设置服务器的ip地址和服务器的端口号。
Java public UdpEchoClient(String serverIp, int serverPort) throws SocketException { socket = new DatagramSocket(); this.serverIp = serverIp; this.serverPort = serverPort; } |
1. 为什么 服务器端必须绑定固定端口?
- 如果服务器端口号不是固定的(每次启动都由系统随机分配),那客户端根本无法确定要连哪个端口 → 无法通信
- 就像 HTTP 服务几乎都用 80 或 8080,DNS 用 53,这是约定俗成的固定端口。
- 一旦端口号固定,客户端只要知道 IP + 端口,无论什么时候启动服务器,客户端都能正常访问。
所以:服务器必须 显式指定端口号,否则客户端没有“入口坐标”
2. 为什么 客户端一般不需要指定端口号?
- 当你 new DatagramSocket() 不传端口时,操作系统会从 临时端口范围(通常是 49152–65535)中自动分配一个空闲端口,供这次通信使用
- 这对客户端来说是合适的:因为它不需要对外“公开”端口号,只需要能拿到一个临时端口来发数据就够了
3. 客户端需不需要端口号?
- 需要的,因为网络通信一定是 IP + 端口 两端才能对应。
- 只是这个端口号通常 不用人工指定,而是由操作系统自动分配。
- 如果你真的想固定客户端端口(比如调试用),也可以写成:
- socket = new DatagramSocket(55555); // 客户端强行绑定端口
4. 总结
- 服务器端:端口必须固定 → 给客户端一个稳定的访问入口
- 客户端:端口不必固定 → 系统分配临时端口即可,只要能发请求
对UDP-API的个人见解
无论是服务器还是客户端,在程序中都需要存在一个socket连接点
我们通过new DatagramSocket()来完成,通过服务器程序Server的构造方法,结合DatagramSocket的构造方法绑定具体的端口号
问题一,为什么需要绑定端口号,而不需要绑定ip地址呢?
对于一个程序来说,它所生活的环境也就是运行的机器是从出生就注定的,而一台主机的ip地址分配工作往往是由DHCP服务器来决定的,也就是操作系统底层已经决定好了
而端口号就是决定了本程序的一个标识,对于电脑上数不胜数的程序,我们自己写的服务器的端口号是程序员自己决定的
我们在服务器端通过一个socket连接点,可以通过一系列api来处理后续的操作
1.作为服务器等待客户端的连接
2.接受客户端发来的数据报[封装]
3.处理数据,并返回给客户端
在等待客户端的连接通过while(true)即可实现
在等待过程中如果有客户端发来消息,我们便可以通过DatagramSocket提供的阻塞等待的receive方法来接受请求
但是需要注意的是receive的参数是被封装的一段数据,我们通过DatagramPacket来实现
DatagramPacket,对于它的参数:
buf – buffer for holding the incoming datagram.我们可以确定需要一个buffer数组,以及它的长度,与我们平常编写Java代码不同,这里的.........
在接收到参数DatagramPacket的实例后,这其实是一段byte类型的数据,不妨将字节数组转化为String字符串便于我们进行后续的处理
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
后续的处理无非就是服务器根据业务进行一定的处理,我们这里的业务很简单只是echo回显数据而已
Java private String process(String request) { return request; } |
经过一系列的处理,到了返回最终客户端数据的时候
由于数据处理后变成了字符串,并不能在socket中进行传输,我们仍然需要重新构建DatagramPacket来进行数据的封装
Java String response = process(request); DatagramPacket responsePacket = new DatagramPacket( response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress() ); |
这里的参数有三个,无非就是转化为字节数组,得到字节数组的长度,getSocketAddress()
在客户端和服务端的代码中都没有close方法,难道socket不需要关闭吗?
Java 在 UDP 服务器代码里: while (true) { // 不断接收、处理、响应 } 这就是一个 典型的长生命周期服务。 DatagramSocket socket = new DatagramSocket(port); 创建好之后,整个服务器运行期间都在用它。 因为 while (true) 是死循环,只要服务器不退出,socket 就一直被占用。 所以不需要在循环里频繁关闭,否则你一关掉,下一次 receive 就无法用了。 那么 socket 什么时候需要关闭呢? 不需要在 start() 方法中主动关闭 socket。 这种设计是正确且高效的,因为 socket 的生命周期与服务器进程完全一致,进程退出时操作系统会自动回收资源。 如果你将来需要实现服务器的优雅关机功能,那时再考虑在跳出循环后手动调用 socket.close()。
Java 标准输入输出流的生命周期。 1. System.out 和 System.in 的本质 System.out 类型是 PrintStream。 默认连向 标准输出(通常是控制台/终端)。 JVM 启动时就初始化好了,是一个全局的单例。 System.in 类型是 InputStream。 默认连向 标准输入(通常是键盘输入)。 同样由 JVM 在启动时初始化,并且在整个 JVM 生命周期中共享。 这两个对象都是 全局静态单例,JVM 在 System 类加载时就准备好了。 2. 为什么我们一般不需要 close()? 关闭了就没法再用 如果你调用 System.in.close(),那么后续所有输入都报错(IOException: Stream closed)。 如果你调用 System.out.close(),那之后就没法再 System.out.println() 了。 JVM 自己会在进程结束时清理 标准输入/输出流不是你自己创建的,而是 JVM 提供的“全局资源”。 JVM 退出时会自动清理它们对应的底层 OS 资源(文件描述符)。 习惯用法 对于自己 new FileInputStream("xxx") 创建的流,需要显式 close(),否则资源泄露。 但 System.in/out/err 属于 JVM 管理的全局流,一般不主动关闭。 3. 官方的说法 Oracle 的文档里有明确提示: It is not recommended to close System.in, System.out, or System.err, because once closed they cannot be reopened. |