【JavaEE初阶】网络编程套接字——Socket、UDP与TCP编程入门



文章目录:
摘要
网络编程的本质,是应用程序借助操作系统提供的 Socket API,通过“网卡”与其他主机通信。本文梳理了Socket的核心概念与两大协议的对比:TCP 有连接、可靠传输、面向字节流;UDP 无连接、不可靠传输、面向数据报,二者均为全双工。在此基础上,分别介绍了 UDP(DatagramSocket / DatagramPacket)和TCP(ServerSocket / Socket)的核心API,并过“回显服务器”和“英译中服务器”两个示例演示了客户端/服务器程序的完整编写流程。

一、Socket套接字
1.概念
操作系统给应用程序提供的一组网络编程的API,称为 socket api。
socket是操作系统中一种“特殊的文件”,表示“网卡”硬件设备
Socket api是传输层提供给应用层
2.分类
- TCP协议*:有连接,可靠传输,面向字节流,全双工*
- **UDP**协议:无连接,不可靠传输,面向数据报,全双工
连接:抽象的,虚拟的连接,描述通信双方的“关系”。
有连接:先保存对方的关键信息,然后发送数据;
无连接:不需要保存对方的关键信息,直接发送数据 (发送数据的函数里,通过参数,指定对方的ip和端口)
可靠传输:数据发出去之后,关心对方是否收到,如果没收到,补救(效率较低)
不可靠传输:数据发出去之后就不管了
面向字节流:TCP和文件类似,可以一次全发,也可以分次发送
面向数据报:UDP传输以“数据报”为单位,进行传输,一次必须发送/接受一个UDP数据报
全双工:同时进行双向通信
半双工:同一时刻,只能单向通信
二、UDP数据报套接字编程
1.API介绍
DatagramSocket
创建一个重要的对象,就相当于在系统内核里创建了一个socket文件。
创建socket文件,也会占用文件描述符表的一个位置,用完之后要及时关闭,否则可能会出现“文件资源泄露”
DatagramSocket 构造⽅法:
| 方法签名 | 方法说明 |
|---|---|
| DatagramSocket() | 创建一个 UDP 数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端) |
| DatagramSocket(int port) | 创建一个 UDP 数据报套接字的 Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket ⽅法:
| 方法签名 | 方法说明 |
|---|---|
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
| void send(DatagramPacket p) | 从此套接字发送数据包(不会阻塞等待,直接发送) |
| void close() | 关闭此数据报套接字 |
DatagramPacket
表示一个UDP的数据报,UDP进行网络传输的基本单位
DatagramPacket 构造⽅法:
| 方法签名 | 方法说明 |
|---|---|
| DatagramPacket(byte[] buf, int length) | 构造一个 DatagramPacket 用来接收数据报,接收的数据保存在字节数组(第一个参数 buf)中,接收指定长度(第二个参数 length) |
| DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个 DatagramPacket 用来发送数据报,发送的数据为字节数组(第一个参数 buf)中,从 0 到指定长度(第二个参数 length)。address 指定目的主机的 IP 和端口号 |
DatagramPacket ⽅法:
| 方法签名 | 方法说明 |
|---|---|
| InetAddress getAddress() | 从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址 |
| int getPort() | 从接收的数据报中,获取发送主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
| byte[] getData() | 获取数据报中的数据 |
2.代码示例
编写一个udp客户端服务器程序 (回显服务器echo server)
udp客户端给udp服务器发送一个字符串(用户控制台输入)
udp服务器收到之后,把这个字符串,原封不动的返回给客户端
服务器:
package network.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
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("启动服务器...");
//服务器需要不断处理客户发来的请求
while (true){
//循环一次,处理一次请求
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);//输出型参数
//把载荷获取出来,构造成string,方便处理请求
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//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(),responsePacket.getPort(),request,response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);//端口号指定范围:0~65535,小于1024也不使用
server.start();
}
}

客户端:
package network.udp;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
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("客户端启动...");
//循环一次,就从控制台读取一个输入的数据,发送到服务器,读取服务器响应
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取用户输入
System.out.println("-> ");
String request = scanner.next();
//2.构造请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(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.打印出response
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}

英译中服务器:只需重写process
package network.udp;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("hello","你好");
dict.put("cat","小猫");
dict.put("dog","你好");
dict.put("pig","你好");
}
//重写process
@Override
public String process(String request) {
//实现英文-->中文
return dict.getOrDefault(request,"[没有找到该单词]");
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9999);
server.start();
}
}

三、TCP流套接字编程
1.API介绍
ServerSocket:给服务器使用的
| 方法签名 | 方法说明 |
|---|---|
| ServerSocket(int port) | 创建一个服务端流套接字 Socket,并绑定到指定端口 |
| 方法签名 | 方法说明 |
|---|---|
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
调用close,目的是为了释放资源(文件描述符表,防止文件资源泄露,确定不会使用了再释放,UDP进程结束系统自动释放,TCP得手动调用)
Socket:客户端/服务器
Socket 构造方法
| 方法签名 | 方法说明 |
|---|---|
| Socket(String host, int port) | 创建一个客户端流套接字 Socket,并与对应 IP 的主机上,对应端口的进程建立连接 |
Socket 方法
| 方法签名 | 方法说明 |
|---|---|
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
2.代码示例
客户端
package network.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort)throws IOException {
socket = new Socket(serverIp,serverPort);
}
public void start() throws IOException {
System.out.println("客户端启动...");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true){
//1.从控制台读取用户输入
System.out.println("-> ");
String request = scannerConsole.next();
//2.构造请求,发给服务器
printWriter.println( request);
printWriter.flush();
//3.从服务器读取响应
if(!scannerNetwork.hasNext()){
break;
}
String response = scannerNetwork.next();
//4.把响应显示到控制台上
System.out.println(response);
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}

服务器
package network.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoSever {
private ServerSocket serverSocket = null;
public TcpEchoSever(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动...");
//创建线程池,Executors.newCachedThreadPool()没有上限
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
//TCP 每次循环先处理连接,
Socket socket = serverSocket.accept();//阻塞等待,如果有客户端给服务器“打电话”,accept就直接接通,如果没有,就等待
//processConnection(socket);//直接调用会存在问题
//主线程负责accept,等待。创建新线程,调用processConnection,读写处理
// Thread t = new Thread(() ->{
// try {
// processConnection(socket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
// t.start();
//还可以创建线程池,进一步减少频繁创建销毁线程的开销
service.submit(() ->{
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void processConnection(Socket socket) throws IOException {
//一个连接中,可能会涉及到多组请求/响应的交互
System.out.printf("[%s,%d] 客户端上线!\n",socket.getInetAddress().toString(),socket.getPort());
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1.读取请求并解析
if(!scanner.hasNext()){
//判断tcp连接是否断开
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端
printWriter.println(response);
printWriter.flush();
//4.打印日志
System.out.printf("[%s:%d] req: %s; resp: %s\n",socket.getInetAddress().toString(), socket.getPort(), request, response);
}
}catch (IOException e) {
e.printStackTrace();
}finally {
try {
System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress().toString(),socket.getPort());
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoSever sever = new TcpEchoSever(9090);
sever.start();
}
}

【注意】
启动多个客户端时,服务器只能感知到一个客户端上线,这时我们就需要用到多线程。主线程负责accept,等待。创建新线程,调用processConnection,读写处理。但是频繁创建销毁线程会消耗资源,影响效率。线程池刚好可以解决这些问题,如上代码。
五、 总结
- Socket 是什么:操作系统提供的网络编程API,本质是代表网卡的“特殊文件”,同样占用文件描述符,用完需及时关闭,否则会资源泄露。
- TCP vs UDP:TCP 先建立连接、关心对方是否收到、以字节流方式分次读写;UDP不保存对方信息、发送即不管、以完整数据报为单位收发。没有优劣之分,按场景选择。
- 编程模型:UDP 用 DatagramSocket 收发 DatagramPacket,无需连接;TCP 服务器用 ServerSocket 监听、accept() 拿到 Socket后像读写文件一样读写字节流。
- 设计技巧:回显服务器只需重写 process方法即可扩展为翻译服务器等业务服务器——把“网络通信骨架”和“业务逻辑”解耦,是服务器代码复用的关键思想。


openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)