第11章 网络编程:让程序聊起来
本章先在本机让两个独立程序通过 UDP 或 TCP 通信。地址、端口、编码、消息边界和结束方式都约定好,再扩展到多客户端和文件上传。
本章路线
- 第1节 网络编程:让两台程序聊起来
1. 网络编程:让两台程序聊起来
同一程序中的方法可以直接调用,两个独立程序通信则要约好地址、端口和消息格式。UDP 像寄明信片;TCP 提供可靠字节流,但一条消息从哪里开始、到哪里结束,还得由应用自己规定。
在本机完成一次通信,再对照 UDP 数据报和 TCP 字节流,解释自己的消息边界是怎样设计的。
1. 网络编程概述
网络通信先约定层次和协议。应用写出去的“一个字符串”还要变成字节,接收端才能按同样规则解释。
网络编程:实现计算机之间数据交换的编程技术。
网络模型
- OSI 七层模型:物理层 → 数据链路层 → 网络层 → 传输层 → 会话层 → 表示层 → 应用层
- TCP/IP 四层模型:网络接口层 → 网际层 → 传输层 → 应用层
Java 主要关注传输层的 TCP 和 UDP 协议。
2. 网络编程三要素
地址决定找哪台主机,端口决定找哪个服务;传输协议也是约定的一部分。
- IP 地址:定位设备(哪个电脑)
- 端口号:定位进程(哪个程序),范围 065535,01023 为系统保留
- 协议:通信规则(TCP/UDP)
IP 地址
// 获取本机 IPInetAddresslocalHost=InetAddress.getLocalHost();System.out.println(localHost.getHostAddress());// 192.168.x.xSystem.out.println(localHost.getHostName());// 主机名// 根据域名获取 IPInetAddressbaidu=InetAddress.getByName("www.baidu.com");System.out.println(baidu.getHostAddress());// 判断是否能 ping 通System.out.println(baidu.isReachable(3000));IPv4 vs IPv6
- IPv4:32 位,4 字节,约 43 亿地址(已耗尽)
- IPv6:128 位,16 字节,号称"地球上每粒沙子都能分一个 IP"
特殊 IP
127.0.0.1/localhost:本机回环地址192.168.x.x:局域网私有地址0.0.0.0:本机所有 IP
端口号
- 0~65535,同一台机器上端口不能冲突
- 知名端口:HTTP(80)、HTTPS(443)、MySQL(3306)、Redis(6379)、SSH(22)
3. UDP 通信
UDP 保留数据报边界,但送达、顺序和重复问题要由应用按需处理。先在本机做最小收发,再谈聊天室。
User Datagram Protocol:用户数据报协议
特点
| 特性 | 说明 |
|---|---|
| 面向无连接 | 不需要建立连接,直接发 |
| 不可靠 | 可能丢包、乱序 |
| 效率高 | 无连接建立开销 |
| 数据报大小 | 最多 64KB |
UDP 发送端
// 1. 创建 DatagramSocket(无需指定端口,系统分配)DatagramSocketsocket=newDatagramSocket();// 2. 将数据打包byte[]data="你好,世界!".getBytes();InetAddressaddress=InetAddress.getByName("127.0.0.1");intport=8888;DatagramPacketpacket=newDatagramPacket(data,data.length,address,port);// 3. 发送socket.send(packet);// 4. 关闭socket.close();UDP 接收端
// 1. 创建 DatagramSocket,绑定端口DatagramSocketsocket=newDatagramSocket(8888);// 2. 创建接收包(指定缓冲区大小)byte[]buf=newbyte[1024*64];// 最大 64KBDatagramPacketpacket=newDatagramPacket(buf,buf.length);// 3. 接收(阻塞等待)socket.receive(packet);// 4. 解析数据byte[]data=packet.getData();intlength=packet.getLength();Stringmessage=newString(data,0,length);System.out.println("收到: "+message);System.out.println("来自: "+packet.getAddress()+":"+packet.getPort());// 5. 关闭socket.close();UDP 通信注意点
- 先启动接收端,再启动发送端
- 发和收是一对一的(一个端口对应一个接收端)
DatagramPacket的缓冲区(buf.length)不要超过 64KB
UDP 实现聊天室
// 发送线程newThread(()->{DatagramSocketsocket=newDatagramSocket();Scannersc=newScanner(System.in);while(true){Stringmsg=sc.nextLine();if("exit".equals(msg))break;byte[]data=msg.getBytes();DatagramPacketpacket=newDatagramPacket(data,data.length,InetAddress.getByName("255.255.255.255"),8888);// 广播地址socket.send(packet);}socket.close();}).start();// 接收线程newThread(()->{DatagramSocketsocket=newDatagramSocket(8888);while(true){byte[]buf=newbyte[1024];DatagramPacketpacket=newDatagramPacket(buf,buf.length);socket.receive(packet);Stringmsg=newString(packet.getData(),0,packet.getLength());System.out.println(packet.getAddress()+": "+msg);}}).start();4. TCP 通信
TCP 提供有序可靠的字节流,应用仍需定义长度、分隔符等消息边界。read 返回一次,并不等于收到一条完整业务消息。
Transmission Control Protocol:传输控制协议
特点
| 特性 | 说明 |
|---|---|
| 面向连接 | 需要三次握手建立连接 |
| 可靠传输 | 确认重传、按序到达 |
| 效率较低 | 有连接建立和维护开销 |
| 无数据大小限制 | 以流的形式传输 |
TCP 客户端
// 1. 创建 Socket,连接服务器(IP + 端口)Socketsocket=newSocket("127.0.0.1",9999);// 2. 获取输出流,发送数据OutputStreamos=socket.getOutputStream();os.write("你好,服务器!".getBytes());// 3. 获取输入流,接收响应InputStreamis=socket.getInputStream();byte[]buf=newbyte[1024];intlen=is.read(buf);System.out.println("服务器响应: "+newString(buf,0,len));// 4. 关闭socket.close();TCP 服务端
// 1. 创建 ServerSocket,绑定端口ServerSocketserverSocket=newServerSocket(9999);// 2. 等待客户端连接(阻塞)Socketsocket=serverSocket.accept();// 3. 获取输入流,读取数据InputStreamis=socket.getInputStream();byte[]buf=newbyte[1024];intlen=is.read(buf);System.out.println("收到: "+newString(buf,0,len));// 4. 获取输出流,回写响应OutputStreamos=socket.getOutputStream();os.write("收到,谢谢!".getBytes());// 5. 关闭socket.close();serverSocket.close();TCP 通信注意点
- 必须先启动服务端,再启动客户端
accept()是阻塞方法,等待客户端连接read()也是阻塞的,直到数据到达或流关闭- 需要约定结束标记(如 shutdownOutput / 约定 \n / 发送特殊字符串)
避免 read() 阻塞的问题
// 方案 1:发送方关闭输出流(通知接收方结束)socket.shutdownOutput();// 发送完数据后调用// 方案 2:约定结束标记// 发送方:每行末尾加 \n// 接收方:用 BufferedReader.readLine() 按行读取// 方案 3:先发送数据长度// 发送:先写 4 字节表示数据长度,再写数据体// 接收:先读前 4 字节得到长度,再读指定字节数TCP 多线程服务端
ServerSocketserverSocket=newServerSocket(9999);while(true){Socketsocket=serverSocket.accept();// 每个客户端开一个线程处理newThread(()->{try{InputStreamis=socket.getInputStream();OutputStreamos=socket.getOutputStream();// 处理通信...socket.close();}catch(IOExceptione){e.printStackTrace();}}).start();}TCP 实现文件上传
// 客户端Socketsocket=newSocket("127.0.0.1",10000);FileInputStreamfis=newFileInputStream("test.jpg");OutputStreamos=socket.getOutputStream();byte[]buf=newbyte[8192];intlen;while((len=fis.read(buf))!=-1){os.write(buf,0,len);}socket.shutdownOutput();// 告诉服务器文件发完了// 接收响应InputStreamis=socket.getInputStream();intlen2=is.read(buf);System.out.println(newString(buf,0,len2));fis.close();socket.close();// 服务端ServerSocketss=newServerSocket(10000);Socketsocket=ss.accept();BufferedInputStreambis=newBufferedInputStream(socket.getInputStream());BufferedOutputStreambos=newBufferedOutputStream(newFileOutputStream(System.currentTimeMillis()+".jpg"));byte[]buf=newbyte[8192];intlen;while((len=bis.read(buf))!=-1){bos.write(buf,0,len);}OutputStreamos=socket.getOutputStream();os.write("上传成功".getBytes());bos.close();socket.close();ss.close();三次握手与四次挥手
三次握手(建立连接):
- 客户端 → 服务器:SYN(同步位),请求连接
- 服务器 → 客户端:SYN + ACK,确认收到,我也发连接
- 客户端 → 服务器:ACK,确认收到,连接建立
四次挥手(断开连接):
- 客户端 → 服务器:FIN,我要断开了
- 服务器 → 客户端:ACK,收到断开请求
- 服务器 → 客户端:FIN,我也要断开了
- 客户端 → 服务器:ACK,收到,连接断开
5. UDP vs TCP 对比
选择协议时,先确认业务是否允许丢失、是否要求有序、怎样看待传输开销,再比较两者。
| 对比项 | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接(三次握手) |
| 可靠性 | 不可靠,可能丢包 | 可靠传输 |
| 速度 | 快 | 相对慢 |
| 数据量 | 64KB 上限 | 无限制(流式) |
| 适用场景 | 视频/语音通话、DNS、游戏 | 文件传输、网页、邮件 |
6. BS 架构(浏览器-服务器)
浏览器和服务器通信通常还遵循 HTTP 等应用层协议。底层能接收字节,只是搭好了进一步理解 Web 的基础。
本质也是 TCP,浏览器充当客户端。
// 简易 BS 服务器ServerSocketss=newServerSocket(8080);Socketsocket=ss.accept();// 读取 HTTP 请求BufferedReaderbr=newBufferedReader(newInputStreamReader(socket.getInputStream()));Stringline;while((line=br.readLine())!=null&&!line.isEmpty()){System.out.println(line);}// 返回 HTTP 响应PrintWriterpw=newPrintWriter(socket.getOutputStream());pw.println("HTTP/1.1 200 OK");pw.println("Content-Type: text/html;charset=utf-8");pw.println();pw.println("<h1>Hello Browser!</h1>");pw.flush();socket.close();ss.close();多线程 BS 服务器:每个请求一个线程,返回静态资源(HTML/CSS/JS/图片)。
7. 总结
给发送、读取、结束和超时各写一句约定,才能让两端都正常收工。
| 知识点 | 核心要点 |
|---|---|
| 三要素 | IP(设备)+ 端口(进程)+ 协议(UDP/TCP) |
| UDP | DatagramSocket + DatagramPacket,无连接,64KB 上限 |
| TCP | Socket + ServerSocket,三次握手,流式可靠传输 |
| 阻塞问题 | read() 阻塞,用 shutdownOutput 或约定结束标记 |
| 文件上传 | TCP 流式传输 + 多线程处理 |
| BS 架构 | TCP + HTTP 协议,浏览器当客户端 |
看图:TCP 给字节流,应用定句号
图中使用换行分隔只是一种应用协议,另一种常见做法是长度前缀。一次 write 与一次 read 没有一一对应关系;传送文件时还应约定长度、大小限制和结束方式。
小练习:把这一节变成自己的代码
做一个本机 TCP 回声服务,以一行作为消息边界;发送后能收到对应回复并正常结束。
卡住时再打开这条提示行协议需要换行与刷新;别让双方都在等对方先关闭输出。
这一章怎样算学会了
把本机回声服务关掉重写一次。确认两端的消息边界一致,再测试正常发送、结束和超时的情况,别让程序一直等下去。