Socket网络编程入门:UDP+TCP双案例手把手教你写服务器
目录前言为什么要学网络编程一、网络编程基础概念1.1 什么是网络编程1.2 发送端与接收端1.4 客户端与服务端二、Socket套接字是什么2.1 套接字的概念2.3 TCP vs UDP 核心对比三、UDP数据报套接字编程3.1 UDP通信模型3.2 核心API介绍DatagramSocketDatagramPacket3.3 实战一Echo回显服务器UDP Echo 服务端UDP Echo 客户端3.4 实战二英译汉词典服务器四、TCP流套接字编程4.1 TCP通信模型4.2 核心API介绍ServerSocketSocket4.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这里给大家整理了一张对比表一目了然对比维度TCP传输控制协议UDP用户数据报协议连接性有连接通信前需三次握手建立无连接直接发送数据可靠性可靠传输确认、重传、流量控制不可靠不保证送达丢包不重传传输形式面向字节流无边界面向数据报有明确边界传输效率相对较低有额外开销高开销小速度快数据大小无限制单次最多64K应用场景网页、文件传输、邮件等需可靠的场景直播、游戏、视频会议等实时性优先场景面试常考TCP和UDP的区别是后端面试高频题一定要记牢三、UDP数据报套接字编程3.1 UDP通信模型UDP是无连接、面向数据报的协议意思就是每次发数据都不用建立连接一次把整个数据报发出去接收端也必须一次收完。Java中使用UDP协议通信主要基于两个类DatagramSocket数据报套接字用来发送和接收数据DatagramPacket数据报包就是具体要发送或接收的数据一次完整的UDP请求-响应流程是这样的接收端先创建DatagramSocket绑定端口调用receive()方法阻塞等待数据发送端创建DatagramSocket构造DatagramPacket包含数据目标IP和端口调用send()发送接收端收到数据receive()方法返回从DatagramPacket中取出数据接收端处理业务构造响应的DatagramPacketsend()发回给发送端发送端调用receive()等待响应收到后处理3.2 核心API介绍DatagramSocketDatagramSocket就是UDP的Socket用于发送和接收UDP数据报。构造方法方法签名说明DatagramSocket()创建UDP套接字绑定本机随机端口一般客户端用DatagramSocket(int port)创建UDP套接字绑定指定端口一般服务端用常用方法方法签名说明void receive(DatagramPacket p)接收数据报没数据就阻塞等待void send(DatagramPacket p)发送数据报直接发不阻塞void close()关闭套接字释放资源DatagramPacketDatagramPacket就是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 MapString, 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介绍ServerSocketServerSocket是创建TCP服务端Socket的API。方法签名说明ServerSocket(int port)创建服务端Socket绑定指定端口Socket accept()监听端口有客户端连接就返回Socket否则阻塞等待void close()关闭套接字SocketSocket就是客户端和服务端建立连接后用来收发数据的对象。不管客户端还是服务端连接建立后都用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获取输入输出流循环读取请求、计算响应、返回响应客户端断开后关闭Socketimport 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会输出类似这样的结果最后一列就是进程PIDTCP 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。建议大家把文中的代码都亲手敲一遍跑起来真正理解数据是怎么收发的。如果这篇文章对你有帮助欢迎点赞收藏关注后续还会更新更多网络编程进阶内容