Socket网络编程入门:UDP+TCP双案例手把手教你写服务器

📅 2026/7/12 23:59:51 👁️ 阅读次数 📝 编程学习
Socket网络编程入门:UDP+TCP双案例手把手教你写服务器

目录

前言:为什么要学网络编程?

一、网络编程基础概念

1.1 什么是网络编程

1.2 发送端与接收端

1.4 客户端与服务端

二、Socket套接字是什么?

2.1 套接字的概念

2.3 TCP vs UDP 核心对比

三、UDP数据报套接字编程

3.1 UDP通信模型

3.2 核心API介绍

DatagramSocket

DatagramPacket

3.3 实战一:Echo回显服务器

UDP Echo 服务端

UDP Echo 客户端

3.4 实战二:英译汉词典服务器

四、TCP流套接字编程

4.1 TCP通信模型

4.2 核心API介绍

ServerSocket

Socket

4.3 实战:Echo回显服务器

TCP Echo 服务端

TCP Echo 客户端

五、服务器性能优化

5.1 为什么需要多线程?

5.2 多线程版本服务器

5.3 线程池版本服务器

六、长短连接详解

短连接

长连接

两者对比

七、常见坑点:端口被占用怎么办?

怎么查哪个进程占了端口?

怎么解决?

八、全文总结


前言:为什么要学网络编程?

大家好,今天我们来聊一个Java后端开发绕不开的话题——网络编程

你有没有想过,我们每天刷抖音、逛淘宝、玩网游,这些数据是怎么从服务器传到你手机上的?背后的技术支撑就是网络编程。学会了Socket套接字,你就能自己写服务器、写客户端,真正理解数据是怎么在网络中传输的。

这篇文章我会用最通俗的语言,配合完整的代码案例,带你从零入门UDP和TCP两种套接字编程。看完你就能动手写出自己的第一个网络程序!

本文面向人群:有Java基础、想入门网络编程的同学

你将收获:Socket概念 + UDP/TCP原理 + 4个完整实战代码 + 常见坑点避坑指南

一、网络编程基础概念

1.1 什么是网络编程

先给大家一个直观的理解:你在浏览器打开优酷看视频,本质上就是通过网络,获取到了网络上的一个视频资源。和你本地打开一个视频文件类似,只是这个资源的来源是网络。

网络编程,就是指网络上的不同主机,通过不同的进程,以编程的方式实现网络通信(也就是网络数据传输)。

划重点:只要是不同进程之间通过网络传输数据,就算网络编程。哪怕是同一台电脑上开两个程序互相发数据,也属于网络编程的范畴。

我们开发的时候,为了方便调试,通常都是在自己电脑上开两个进程模拟客户端和服务端。但真实场景下,肯定是分布在不同主机上的:

  • 进程A:负责获取网络资源(比如你的浏览器)

  • 进程B:负责提供网络资源(比如网站的服务器)

1.2 发送端与接收端

在一次网络数据传输中:

  • 发送端:数据的发送方进程,发送端所在的主机叫源主机

  • 接收端:数据的接收方进程,接收端所在的主机叫目的主机

这里要注意,发送端和接收端是相对的概念,只是一次数据传输产生流向后的叫法。A给B发数据时,A是发送端、B是接收端;反过来B给A回数据时,B就变成了发送端、A变成了接收端。

1.4 客户端与服务端

在常见的网络场景中,我们把提供服务的一方叫服务端,获取服务的一方叫客户端

服务端一般提供两种服务:

  1. 客户端获取资源:比如你刷B站,视频资源都在服务器上,你作为客户端去取

  2. 客户端保存资源:比如你发朋友圈,把图片文字上传到微信服务器存起来

最经典的客户端-服务端模型流程是这样的:

  1. 客户端先发送请求到服务端

  2. 服务端根据请求数据,执行业务处理

  3. 服务端返回响应:发送处理结果

  4. 客户端根据响应数据,展示处理结果

二、Socket套接字是什么?

2.1 套接字的概念

Socket(套接字),是操作系统提供的用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。简单说,Socket就是操作系统给我们程序员提供的一套API,让我们能方便地进行网络数据传输。

基于Socket套接字的网络程序开发,就是我们说的网络编程。

2.3 TCP vs UDP 核心对比

很多同学刚学的时候容易搞混TCP和UDP,这里给大家整理了一张对比表,一目了然:

对比维度

TCP(传输控制协议)

UDP(用户数据报协议)

连接性

有连接,通信前需三次握手建立

无连接,直接发送数据

可靠性

可靠传输,确认、重传、流量控制

不可靠,不保证送达,丢包不重传

传输形式

面向字节流,无边界

面向数据报,有明确边界

传输效率

相对较低,有额外开销

高,开销小速度快

数据大小

无限制

单次最多64K

应用场景

网页、文件传输、邮件等需可靠的场景

直播、游戏、视频会议等实时性优先场景

面试常考:TCP和UDP的区别是后端面试高频题,一定要记牢!

三、UDP数据报套接字编程

3.1 UDP通信模型

UDP是无连接、面向数据报的协议,意思就是每次发数据都不用建立连接,一次把整个数据报发出去,接收端也必须一次收完。

Java中使用UDP协议通信,主要基于两个类:

  • DatagramSocket:数据报套接字,用来发送和接收数据

  • DatagramPacket:数据报包,就是具体要发送或接收的数据

一次完整的UDP请求-响应流程是这样的:

  1. 接收端先创建DatagramSocket,绑定端口,调用receive()方法阻塞等待数据

  2. 发送端创建DatagramSocket,构造DatagramPacket(包含数据+目标IP和端口),调用send()发送

  3. 接收端收到数据,receive()方法返回,从DatagramPacket中取出数据

  4. 接收端处理业务,构造响应的DatagramPacket,send()发回给发送端

  5. 发送端调用receive()等待响应,收到后处理

3.2 核心API介绍

DatagramSocket

DatagramSocket就是UDP的Socket,用于发送和接收UDP数据报。

构造方法:

方法签名

说明

DatagramSocket()

创建UDP套接字,绑定本机随机端口(一般客户端用)

DatagramSocket(int port)

创建UDP套接字,绑定指定端口(一般服务端用)

常用方法:

方法签名

说明

void receive(DatagramPacket p)

接收数据报,没数据就阻塞等待

void send(DatagramPacket p)

发送数据报,直接发不阻塞

void close()

关闭套接字,释放资源

DatagramPacket

DatagramPacket就是UDP发送和接收的数据报包。

构造方法:

方法签名

说明

DatagramPacket(byte[] buf, int length)

构造接收用的数据报,数据存buf里,指定接收长度

DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)

构造发送用的数据报,指定数据+目标地址

常用方法:

方法签名

说明

InetAddress getAddress()

获取对方的IP地址

int getPort()

获取对方的端口号

byte[] getData()

获取数据报中的数据

3.3 实战一:Echo回显服务器

我们先来写一个最简单的回显服务器:客户端发什么,服务器就原样返回什么。这是网络编程的"Hello World"。

UDP Echo 服务端

服务器的核心逻辑就是三步走:

  1. 读取请求并解析:receive()接收客户端发来的数据报

  2. 根据请求计算响应:回显服务就是请求等于响应

  3. 把响应写回客户端:构造响应数据报,send()发回

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("服务器启动!"); while (true) { // 反复执行针对客户端请求的处理逻辑 // 1. 读取请求并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(requestPacket); // 转成字符串,约定客户端发的是文本 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); // 打印日志,方便观察 System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response); } } // 根据请求计算响应,回显就是原样返回 public String process(String request) { return request; } public static void main(String[] args) throws IOException { UdpEchoServer server = new UdpEchoServer(9090); server.start(); } }

UDP Echo 客户端

客户端的逻辑也是四步走:

  1. 从控制台读取用户输入

  2. 构造请求对象,发给服务器

  3. 读取服务器的响应

  4. 显示到屏幕上

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 ip, int port) throws SocketException { serverIp = ip; serverPort = port; // 客户端不指定端口,系统自动分配空闲端口 socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("客户端启动!"); while (true) { // 1. 从控制台读取用户输入 System.out.print("-> "); String request = scanner.next(); // 2. 构造请求对象,发给服务器 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp), serverPort); socket.send(requestPacket); // 3. 读取服务器响应 DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); 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("127.0.0.1", 9090); client.start(); } }

运行方式:先启动服务端,再启动客户端,在客户端控制台输入文字,回车就能看到服务器回显回来的内容。注意127.0.0.1是本机回环地址,本地调试用。

3.4 实战二:英译汉词典服务器

学会了回显服务器,我们来升级一下,做一个英译汉词典服务器。客户端输入英文单词,服务器返回对应的中文翻译。

这里我们用继承的方式,直接复用UdpEchoServer的代码,只需要重写process方法就行,非常方便!

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("cat", "小猫"); dict.put("dog", "小狗"); dict.put("hello", "你好"); dict.put("world", "世界"); dict.put("java", "爪哇编程语言"); // 可以继续加千千万万个单词... } // 重写process方法,实现查词典的逻辑 @Override public String process(String request) { // 查到就返回翻译,查不到返回提示 return dict.getOrDefault(request, "该词没有查询到!"); } public static void main(String[] args) throws Exception { UdpDictServer server = new UdpDictServer(9090); server.start(); } }

客户端不用改,直接用之前的UdpEchoClient就行,输入英文单词就能拿到翻译了。

设计思想:你看,通过继承+重写,我们把网络通信的通用逻辑和业务逻辑分开了。以后不管做什么业务,只要重写process方法就行,网络部分完全不用动。这就是代码复用的魅力!

四、TCP流套接字编程

4.1 TCP通信模型

TCP是有连接、面向字节流的协议。和UDP不同,TCP通信前必须先建立连接,就像打电话先拨号接通了才能说话。

Java中TCP通信主要用两个类:

  • ServerSocket:服务端用的,负责监听端口,等待客户端连接

  • Socket:双方建立连接后,用Socket来收发数据,通过输入输出流操作

TCP通信的基本流程:

  1. 服务端创建ServerSocket,绑定端口,调用accept()阻塞等待客户端连接

  2. 客户端创建Socket,指定服务端IP和端口,发起连接请求

  3. 连接建立成功后,服务端accept()返回一个Socket对象

  4. 双方通过Socket获取InputStream和OutputStream,像读写文件一样收发数据

  5. 通信结束,关闭Socket释放资源

4.2 核心API介绍

ServerSocket

ServerSocket是创建TCP服务端Socket的API。

方法签名

说明

ServerSocket(int port)

创建服务端Socket,绑定指定端口

Socket accept()

监听端口,有客户端连接就返回Socket,否则阻塞等待

void close()

关闭套接字

Socket

Socket就是客户端和服务端建立连接后,用来收发数据的对象。不管客户端还是服务端,连接建立后都用Socket来通信。

方法签名

说明

Socket(String host, int port)

创建客户端Socket,连接指定主机的指定端口

InetAddress getInetAddress()

返回连接的对端地址

InputStream getInputStream()

获取输入流,用来读数据

OutputStream getOutputStream()

获取输出流,用来写数据

注意:TCP是面向字节流的,所以我们是通过InputStream和OutputStream来读写数据,就和操作本地文件的流一模一样,非常好上手!

4.3 实战:Echo回显服务器

我们同样来写一个TCP版本的Echo回显服务器,对比一下和UDP版本有什么不同。

TCP Echo 服务端

服务端的思路:

  1. 创建ServerSocket绑定端口

  2. 调用accept()等待客户端连接

  3. 连接建立后,通过Socket获取输入输出流

  4. 循环读取请求、计算响应、返回响应

  5. 客户端断开后,关闭Socket

import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class TcpEchoServer { private ServerSocket serverSocket = null; // 绑定端口号 public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } // 启动服务器 public void start() throws IOException { System.out.println("服务器启动!"); while (true) { // 等待客户端连接 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } } // 处理一个连接的逻辑 private void processConnection(Socket clientSocket) { System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); // 获取输入输出流 try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { // 一次连接中可能有多次请求响应 while (true) { // 1. 读取请求,用Scanner方便读取文本 Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { // 读取完毕,客户端下线 System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回客户端,用PrintWriter方便写文本 PrintWriter writer = new PrintWriter(outputStream); // 用println带上换行,方便客户端scanner读取 writer.println(response); writer.flush(); // 刷新缓冲区,确保数据真的发出去 // 打印日志 System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } } catch (IOException e) { e.printStackTrace(); } finally { // 确保关闭socket释放资源 try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }

TCP Echo 客户端

import java.io.*; import java.net.Socket; import java.util.Scanner; public class TcpEchoClient { private Socket socket = null; // 创建Socket时就会建立TCP连接 public TcpEchoClient(String serverIp, int serverPort) throws IOException { socket = new Socket(serverIp, serverPort); } public void start() { System.out.println("客户端启动"); Scanner scannerConsole = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { while (true) { // 1. 从控制台输入 System.out.print("-> "); String request = scannerConsole.next(); // 2. 把请求发给服务器 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request); printWriter.flush(); // 别忘了flush! // 3. 读取服务器响应 Scanner scannerNetwork = new Scanner(inputStream); 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(); } }

注意两个坑:

1. 一定要用println而不是print,末尾带换行,这样对方的scanner.next()才能正确读取

2. 写完一定要flush()!输出流有缓冲区,不刷新数据可能还在缓冲区里没发出去

五、服务器性能优化

5.1 为什么需要多线程?

刚才写的TCP服务器有个大问题:单线程的,一次只能处理一个客户端连接

因为accept()接收到一个连接后,就进入processConnection()方法循环处理这个客户端的请求了。在这个客户端断开之前,accept()不会再被调用,新的客户端就连不上了。

这肯定不行,真实的服务器要同时服务成百上千个客户端。怎么解决?很简单,每个客户端连接分配一个线程,让它们并发处理。

5.2 多线程版本服务器

改造很简单,只需要修改start方法,每次accept到一个连接,就开一个新线程去处理:

public void start() throws IOException { System.out.println("服务器启动!"); while (true) { Socket clientSocket = serverSocket.accept(); // 每个客户端连接开一个线程处理 Thread t = new Thread(() -> { processConnection(clientSocket); }); t.start(); } }

就改这么几行,服务器就能同时处理多个客户端了!主线程只负责接客(accept),接到了就交给子线程去服务,主线程继续接下一个。

5.3 线程池版本服务器

多线程版本虽然能用,但如果客户端特别多,频繁创建销毁线程开销很大。这时候就可以用线程池来优化,复用线程减少开销。

public void start() throws IOException { System.out.println("服务器启动!"); // 创建一个缓存线程池 ExecutorService service = Executors.newCachedThreadPool(); while (true) { Socket clientSocket = serverSocket.accept(); // 提交任务到线程池 service.submit(new Runnable() { @Override public void run() { processConnection(clientSocket); } }); } }

小知识:newCachedThreadPool会自动创建线程,空闲线程会被回收,适合短任务多的场景。如果是长连接场景,可能需要自定义线程池参数更合适。

六、长短连接详解

TCP是有连接的,那连接什么时候关呢?这就引出了短连接长连接的概念。

短连接

每次收发完一次数据就关闭连接,就是短连接。也就是说,短连接一次只能收发一次数据。下次要发数据,得重新建立连接。

长连接

不关闭连接,一直保持连接状态,双方不停的收发数据,就是长连接。长连接可以多次收发数据。

两者对比

对比项

短连接

长连接

建连关连开销

每次请求都要建连关连,开销大

只建一次连接,后续直接传输,开销小

主动发请求

一般是客户端主动发

客户端和服务端都可以主动发

适用场景

请求频率不高,如浏览网页

通信频繁,如聊天室、实时游戏

扩展了解:我们上面写的Echo服务器用的就是长连接模式,客户端连上后可以一直发消息。而HTTP协议早期用的是短连接,现在HTTP/1.1默认也是长连接了。

另外,基于BIO(同步阻塞IO)的长连接每个连接占一个线程,并发高了扛不住。真正的高并发服务一般用NIO(同步非阻塞IO)来实现长连接,性能会好很多。

七、常见坑点:端口被占用怎么办?

初学Socket编程,最容易遇到的报错就是端口被占用。如果你看到这个异常:

java.net.BindException: Address already in use: JVM_Bind

不用慌,就是你要绑定的端口已经被别的进程占了。一个端口同一时间只能被一个进程绑定。

怎么查哪个进程占了端口?

Windows下打开cmd,输入命令:

netstat -ano | findstr 8888

会输出类似这样的结果,最后一列就是进程PID:

TCP 0.0.0.0:8888 0.0.0.0:0 LISTENING 4588

然后打开任务管理器,在详细信息里按PID排序,找到PID为4588的进程,就知道是谁占了端口。

怎么解决?

  1. 占端口的进程不需要了:直接结束那个进程,再启动你的程序就行

  2. 占端口的进程还要用:改你自己程序的端口号,换一个没被占用的端口

小技巧:一般开发调试用8080、9090、8888这些端口比较多,被占用了就换个数字试试,比如9091、9092。

最后,我们来梳理一下这篇文章的核心知识点,帮大家巩固记忆:

  1. 网络编程基础:网络编程就是不同主机的进程通过网络传输数据。核心概念有发送端/接收端、请求/响应、客户端/服务端。

  2. Socket套接字:操作系统提供的网络通信API,主要分两类:

    1. UDP数据报套接字:无连接、不可靠、面向数据报、速度快

    2. TCP流套接字:有连接、可靠传输、面向字节流、稳定可靠

  3. UDP编程:核心类是DatagramSocket和DatagramPacket。服务端循环receive接收请求→process处理→send返回响应。

  4. TCP编程:核心类是ServerSocket和Socket。服务端accept等待连接→建立连接后通过InputStream/OutputStream读写数据,和操作文件流一模一样。

  5. 服务器优化:单线程服务器一次只能处理一个客户端,用多线程或线程池可以实现并发处理。

  6. 长短连接:短连接一次请求一关连接,长连接保持连接多次通信。请求频率低用短连接,通信频繁用长连接。

  7. 常见坑:端口被占用报BindException,用netstat查PID结束进程或换端口。


网络编程是Java后端的基础中的基础,不管是做Web开发、微服务还是分布式系统,底层都离不开Socket。建议大家把文中的代码都亲手敲一遍跑起来,真正理解数据是怎么收发的。

如果这篇文章对你有帮助,欢迎点赞收藏关注,后续还会更新更多网络编程进阶内容!