目录
前言:为什么要学网络编程?
一、网络编程基础概念
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 客户端与服务端
在常见的网络场景中,我们把提供服务的一方叫服务端,获取服务的一方叫客户端。
服务端一般提供两种服务:
客户端获取资源:比如你刷B站,视频资源都在服务器上,你作为客户端去取
客户端保存资源:比如你发朋友圈,把图片文字上传到微信服务器存起来
最经典的客户端-服务端模型流程是这样的:
客户端先发送请求到服务端
服务端根据请求数据,执行业务处理
服务端返回响应:发送处理结果
客户端根据响应数据,展示处理结果
二、Socket套接字是什么?
2.1 套接字的概念
Socket(套接字),是操作系统提供的用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。简单说,Socket就是操作系统给我们程序员提供的一套API,让我们能方便地进行网络数据传输。
基于Socket套接字的网络程序开发,就是我们说的网络编程。
2.3 TCP vs UDP 核心对比
很多同学刚学的时候容易搞混TCP和UDP,这里给大家整理了一张对比表,一目了然:
|
连接性 |
有连接,通信前需三次握手建立 |
无连接,直接发送数据 |
|
可靠性 |
可靠传输,确认、重传、流量控制 |
不可靠,不保证送达,丢包不重传 |
|
传输形式 |
面向字节流,无边界 |
面向数据报,有明确边界 |
|
传输效率 |
相对较低,有额外开销 |
高,开销小速度快 |
|
数据大小 |
无限制 |
单次最多64K |
|
应用场景 |
网页、文件传输、邮件等需可靠的场景 |
直播、游戏、视频会议等实时性优先场景 |
面试常考:TCP和UDP的区别是后端面试高频题,一定要记牢!
三、UDP数据报套接字编程
3.1 UDP通信模型
UDP是无连接、面向数据报的协议,意思就是每次发数据都不用建立连接,一次把整个数据报发出去,接收端也必须一次收完。
Java中使用UDP协议通信,主要基于两个类:
-
DatagramSocket:数据报套接字,用来发送和接收数据
-
DatagramPacket:数据报包,就是具体要发送或接收的数据
一次完整的UDP请求-响应流程是这样的:
接收端先创建DatagramSocket,绑定端口,调用receive()方法阻塞等待数据
发送端创建DatagramSocket,构造DatagramPacket(包含数据+目标IP和端口),调用send()发送
接收端收到数据,receive()方法返回,从DatagramPacket中取出数据
接收端处理业务,构造响应的DatagramPacket,send()发回给发送端
发送端调用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 服务端
服务器的核心逻辑就是三步走:
读取请求并解析:receive()接收客户端发来的数据报
根据请求计算响应:回显服务就是请求等于响应
把响应写回客户端:构造响应数据报,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 客户端
客户端的逻辑也是四步走:
从控制台读取用户输入
构造请求对象,发给服务器
读取服务器的响应
显示到屏幕上
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通信的基本流程:
服务端创建ServerSocket,绑定端口,调用accept()阻塞等待客户端连接
客户端创建Socket,指定服务端IP和端口,发起连接请求
连接建立成功后,服务端accept()返回一个Socket对象
双方通过Socket获取InputStream和OutputStream,像读写文件一样收发数据
通信结束,关闭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 服务端
服务端的思路:
创建ServerSocket绑定端口
调用accept()等待客户端连接
连接建立后,通过Socket获取输入输出流
循环读取请求、计算响应、返回响应
客户端断开后,关闭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的进程,就知道是谁占了端口。
怎么解决?
占端口的进程不需要了:直接结束那个进程,再启动你的程序就行
占端口的进程还要用:改你自己程序的端口号,换一个没被占用的端口
小技巧:一般开发调试用8080、9090、8888这些端口比较多,被占用了就换个数字试试,比如9091、9092。
最后,我们来梳理一下这篇文章的核心知识点,帮大家巩固记忆:
网络编程基础:网络编程就是不同主机的进程通过网络传输数据。核心概念有发送端/接收端、请求/响应、客户端/服务端。
Socket套接字:操作系统提供的网络通信API,主要分两类:
UDP数据报套接字:无连接、不可靠、面向数据报、速度快
TCP流套接字:有连接、可靠传输、面向字节流、稳定可靠
UDP编程:核心类是DatagramSocket和DatagramPacket。服务端循环receive接收请求→process处理→send返回响应。
TCP编程:核心类是ServerSocket和Socket。服务端accept等待连接→建立连接后通过InputStream/OutputStream读写数据,和操作文件流一模一样。
服务器优化:单线程服务器一次只能处理一个客户端,用多线程或线程池可以实现并发处理。
长短连接:短连接一次请求一关连接,长连接保持连接多次通信。请求频率低用短连接,通信频繁用长连接。
常见坑:端口被占用报BindException,用netstat查PID结束进程或换端口。
网络编程是Java后端的基础中的基础,不管是做Web开发、微服务还是分布式系统,底层都离不开Socket。建议大家把文中的代码都亲手敲一遍跑起来,真正理解数据是怎么收发的。
如果这篇文章对你有帮助,欢迎点赞收藏关注,后续还会更新更多网络编程进阶内容!
网硕互联帮助中心




评论前必须登录!
注册