文章目录
- 一、网络基础概念
-
- [1.1局域网 vs 广域网](#1.1局域网 vs 广域网)
- 1.2交换机和路由器
- [1.3 IP地址和端口号](#1.3 IP地址和端口号)
- 1.4协议
- 1.5五元组
- 1.6协议分层
- 二、应用层
- 三、传输层
-
- [3.1 TCP和UDP](#3.1 TCP和UDP)
-
- 1.有连接VS无连接
- 2.可靠传输VS不可靠传输
- [3.面向字节流 VS 面向数据报](#3.面向字节流 VS 面向数据报)
- [4. 全双工 VS 半双工](#4. 全双工 VS 半双工)
- [3.2 网络编程](#3.2 网络编程)
一、网络基础概念
1.1局域网 vs 广域网
| 对比点 | 局域网 (LAN) | 广域网 (WAN) |
|---|---|---|
| 范围 | 小(一栋楼/一个校园) | 大(跨城市/国家) |
| 所有权 | 自己拥有 | 运营商租用 |
| 速度 | 快(千兆+) | 慢(相对) |
| IP地址 | 私有IP(如192.168.x.x) | 公网IP或NAT转换 |
| 一句话 | 自己家院子喊话 | 邮局寄信到全世界 |
1.2交换机和路由器
交换机和路由器都是组件网络的核心设备。
路由器图片如下:

WAN口(广域网口):通常只有一个,作用是连接外网。家用路由器一般是用来连接运营商设备。
LAN(局域网口):作用是给家里的设备提供有线网络。
交换机图片如下:

交换机上面有非常多的插口(可以想象成插排),相当于路由器的拓展,在路由器上插入一个交换机就可以拓展出很多接口,如果交换机插口不够用,交换机还可以继续再连交换机。
1.3 IP地址和端口号
IP地址:表示网络一台设备所在的位置。
端口号:一台主机可能同时有多个程序在使用网络,端口号可以区分这些应用程序。
举个例子:我购买了一个快递,商家需要知道我的收货地址(云南大学)才能给我发货,这个收获地址就是我的IP地址,到达了云南大学,大学里面有很多人,要准确的送到我的手上,快递小哥就可以通过我的手机号给我打电话,让我来取快递,此时我的手机号码就是端口号,用来区分我和别人的快递。
1.4协议
多台主机,约定同一个标准,大家都共同遵守,这就是网络协议。
举个例子:我遵守说普通话的约定,你也说普通话,我们之间就能相互沟通交流通信,普通话就是我们之间的协议。如果我说普通话,你说方言,我无法解析出你说的方言, 就无法进行沟通。
1.5五元组
在TCP/TP中,用五元组来表示一个网络通信:
1.源IP: 标识源主机
2.源端口号:标识源主机中该次通信发送数据的进程
3.目的IP: 标识目的主机
4.目的端口:标识⽬的主机中该次通信接收数据的进程
5.协议号:标识发送进程和接收进程双方约定的数据格式

举个例子:发快递

1.6协议分层
分层降低了网络设计的复杂度,每一层只做一件事,上下层通过接口协作,互不干扰。
在实际网络工作中,我们分为五层TCP/IP协议网络模型。
1.五层TCP/IP协议

通俗理解:
1.物理层规定了网络通信的一些硬件设施符合的要求
2.数据链路层:完成两个相邻设备之间是如何通信,通过网线,把电脑连到路由器上。
3.网络层:任意两个设备之间如何通信,比如两个设备之间可能隔着很多路由器和交换机。考虑通讯的过程是怎么样的。
4.传输层:也是任意两个设备之间如何通信,但是不考虑通讯的过程是怎么样的。
5.应用层:东西如何使用。
举个例子:打包快递发出去

一层一层的对数据进行封装,直到发出去。
2.主机,路由器和交换机是工作在哪一层?
主机全都要,路由到网络,交换机只管两层。
主机: 物理层 → 应用层 ,全层参与
路由器: 物理层 → 网络层, 不关心端口、不关心进程
交换机: 物理层 → 数据链路层 , 不认识IP,只认MAC地址(物理地址)
3.TCP/IP通讯的封装过程
示例:从应用程序上发送hello给对方
此处假设协议格式是:发送者ID,接受者ID,消息时间,消息正文
假设数据为:1234567,7654321,2026-4-27 20:00:00,hello
发送的过程是一步一步的对数据进行封装的过程

4.TCP/IP通讯的分用过程
示例:对方主机接收到数据,逐层进行分用

5.传输中间过程的封装分用
交换机 :只需要封装分用到数据链路层
主机的数据 :交换机收到后,物理层解析,数据链路层解析,重新构造出以太网数据帧,发送给下一个设备的数据链路层中,得到以太网数据帧的帧头,信息就足以支持交换机进行下一步工作。交换机是工作在数据链路层,二层转发。
主机的数据 :路由器收到后,物理层解析,数据链路层解析,网络层解析,重新构造出新的网络数据包,构造出以太网数据帧,构造出二进制数据,进行转发。路由器是工作在网络层,三层转发。
二、应用层
操作系统提供一组socket api(传输层给应用层提供)
三、传输层
3.1 TCP和UDP
传输层两组核心协议:TCP和UDP
TCP :有连接,可靠传输,面向字节流,全双工
UDP:无链接,不可靠传输,面向数据报,全双工
1.有连接VS无连接
有连接 :TCP协议保存了对端的信息。比如:A与B建立连接,A保存了B的信息,B保存了A的信息。
无连接:UDP协议本身不保存对方的信息。
2.可靠传输VS不可靠传输
网络上数据传输是非常容易丢包的,不管是光信号还是电信号都会受到干扰,不能保证一个数据包发送后100%到达对方。
可靠传输 :不是保证一个数据包发送后100%到达对方,而是尽可能提高传输成功概率。
不可靠传输:只是把数据发了,就不管了。
3.面向字节流 VS 面向数据报
面向字节流 :读取的时候是以字节为单位的。
面向数据报:读写数据的时候,是以数据报为单位的。
4. 全双工 VS 半双工
全双工 :一个通信链路,支持双向通信(能读,也能写)
半双工:一个通信链路,支持单向通信(要么读,要么写)
注:TCP 和 UDP 都是全双工协议。只有少数传统协议(如 RS‑485 半双工模式)或特殊实现才是半双工。
3.2 网络编程
1.UDP数据报套接字编程
DatagramPacket 构造方法
| 方法签名 | 方法说明 |
|---|---|
DatagramPacket(byte[] buf, int length) |
构造一个 DatagramPacket 以用来接收数据报。接收的数据保存在字节数组 buf 中,最多接收 length 字节。 |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个 DatagramPacket 以用来发送数据报。发送的数据为字节数组 buf 中从 offset 开始的 length 字节。address 指定目标主机的 IP 和端口号。 |
DatagramPacket 方法
| 方法签名 | 方法说明 |
|---|---|
InetAddress getAddress() |
从接收的数据报中获取发送端主机的 IP 地址;或从发送的数据报中获取接收端主机的 IP 地址。 |
int getPort() |
从接收的数据报中获取发送端主机的端口号;或从发送的数据报中获取接收端主机的端口号。 |
byte[] getData() |
获取数据报中的数据(字节数组)。 |
前两个表格方法都是UDP专用。
InetSocketAddress 构造方法
| 方法签名 | 方法说明 |
|---|---|
InetSocketAddress(InetAddress addr, int port) |
创建一个 Socket 地址,包含 IP 地址 addr 和端口号 port。InetSocketAddress 是 SocketAddress 的子类。 |
使用提示:构造 UDP 发送数据报时,需要传入
SocketAddress对象,可使用InetSocketAddress来创建。UDP和TCP都通用。
2.UDP代码演示
回显服务器
java
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket = null;
//打开UDP服务器
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//循环一次就相当于处理一次请求
//处理请求的过程,典型的服务器一般分为三个步骤
// 1.读取请求并解析
// 构造请求数据报
// DatagramPacket 表示一个UDP数据报 = 报头+载荷 此处传入的字节数组,就保存UDP的载荷部分
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
// "输出型参数"
socket.receive(requestPacket);
//把读取到的二进制转换成字符串,只是构造有效的部分
// requestPacket.getData() 拿到DatagramPacket中的字节数组
// requestPacket.getLength() 拿到有效的数据长度
// 根据字节数组,构造出一个 String
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
// 2.根据请求,计算响应(服务器最关键的逻辑)
//但是这里写的是回显服务器,所以相当于这步省略了
String response = process(request);
// 3.把响应返回给客户端
// 根据 response构造DatagramPacket,发送给客户端
// response.getBytes() 拿到字符串中的字节数组
// response.getBytes().length 拿到字节数组的长度,而不是使用字符串长度(单位:字符)
// requestPacket.getSocketAddress() 拿到客户端的IP和端口号
// new DatagramPacket 构造响应数据包
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length, requestPacket.getSocketAddress());
// 此处还不能直接发送,UDP协议自身没有保存对方的信息(不知道发给谁)
// 需要指定目的ip 和 目的端口 ,在上面这个DatagramPacket中传入 requestPacket.getSocketAddress()
socket.send(responsePacket);
// 4.打印一个日志
// 记录这次客户端/服务器的交互过程
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();
}
}
客户端
java
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
// UDP不保存对端的信息,所以自己手动写一下
private String serverIp;
private int severPort;
// 和服务器不同,此处的构造方法是要指定访问的服务器的ip和端口
public UdpEchoClient(String serverIp ,int severPort) throws SocketException {
this.serverIp = serverIp;
this.severPort = severPort;
// 这里不能使用severPort,要使用无参的,操作系统随机分配一个端口
// 不推荐客服端固定端口号
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while (true){
// 1.从客户端读取用户输入的内容
System.out.println("请输入要发送的内容");
if(!sc.hasNext()){
break;
}
String request = sc.next();
// 2.把请求发给服务器
// 在构造过程中,我们不仅要构造载荷,还要构造服务器的ip和端口号
// InetAddress.getByName(serverIp) 按字符串的方式来转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),severPort);
// 3.发送数据报
socket.send(requestPacket);
// 4.接收服务器的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
// 5.从服务器读取的信息进行解析打印
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
打印结果:


3. TCP流套接字编程
构造方法
| 方法签名 | 方法说明 |
|---|---|
ServerSocket(int port) |
创建一个服务端流套接字,并绑定到指定端口。 |
方法
| 方法签名 | 方法说明 |
|---|---|
Socket accept() |
开始监听指定端口(构造时绑定的端口)。当有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接;否则阻塞等待。 |
void close() |
关闭此套接字。 |
Socket 类(TCP 客户端 / 服务端连接套接字)
说明:
Socket既可作为客户端套接字(通过构造方法直接连接服务器),也可作为服务端调用accept()后返回的与客户端通信的套接字。
构造方法
| 方法签名 | 方法说明 |
|---|---|
Socket(String host, int port) |
创建一个客户端流套接字,并与指定 IP 主机上对应端口的进程建立连接。 |
这里的参数是服务器的ip 和端口号。
方法
| 方法签名 | 方法说明 |
|---|---|
InetAddress getInetAddress() |
返回套接字所连接的远程地址。 |
InputStream getInputStream() |
返回此套接字的输入流(用于接收对方数据)。 |
OutputStream getOutputStream() |
返回此套接字的输出流(用于向对方发送数据)。 |
4.TCP 代码演示
TcpEchoClient客户端
java
package network;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
// 和服务器的Socket不是同一个对象,相当于打电话的两端
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
// 可以直接把字符串的ip地址设置进来
// 创建socket对象,就会在底层保存对端的ip和端口号,建立tcp连接
socket = new Socket(serverIp,serverPort);
}
public void start(){
// 从客户端读取请求,发送给服务器
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream =socket.getOutputStream()){
// 为了使用方便,所以可以套一层
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
// 1.从控制台读取用户输入
String request = scanner.next();
// 2.发送给服务器
// 这步是把数据放到到"缓冲区"中,还没有真正的写入网卡里
writer.println(request);
// 所以需要冲刷一下
writer.flush();
// 3.读取服务器的响应
String response = scannerNet.next();
// 4.打印到控制台
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
TcpEchoServer服务器
java
package network;
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.nio.charset.StandardCharsets;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 这里和UDP服务器类似,也是在构造方法的时候绑定服务器端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
// 对于TCP,需要先处理客户端发来的连接
// 通过读写clientSocket来和客户端进行通信
// 如果没有客户端发来连接,此时accept就会发生堵塞
// clientSocket每个客户端连接都会创建一个新的,每个客户端断开连接,这个对象也可以不要了,所以要关闭,以防文件资源泄露
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
// 处理一个客户端的连接
// 可能设计到多个客户端的连接和请求,这里暂时不写
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
// 获取输入流输出流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream= clientSocket.getOutputStream()) {
// 对inputStream套了一层
Scanner scanner = new Scanner(inputStream);
// 对OutputStream套了一层
PrintWriter writer = new PrintWriter(outputStream);
while (true){
// 1.读取请求并解析 可以用read,也可以用Scanner,Scanner构造方法填入的是InputStream对象
// byte[] request = new byte[1024];
// inputStream.read(request);
if(!scanner.hasNext()){
// 连接断开了
System.out.printf("[%s:%d]客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
String request = scanner.next();
// 2.根据请求计算响应
String response = process(request);
// 3.返回响应到客户端
//outputStream.write(response.getBytes());
writer.println(response);
writer.flush();
// 打印日志
System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
里面的flush操作非常关键!!!
println 一个请求/响应是暗暗约定以ln换行作为结束标志,也就是读到\n就结束了(认为是读到了一个完整的请求)
上面的这个代码只能处理只有一个客户端请求的情况,不能处理发送多个客户端请求的情况,可以加入线程池进行代码优化
IDEA默认是单线程,所以需要手动设置一下


示例:在服务器中加入线程池
java
package network;
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.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
// 这里和UDP服务器类似,也是在构造方法的时候绑定服务器端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
// 这种情况一般不会使用fixedThreadPool
ExecutorService executorService = Executors.newCachedThreadPool();
while (true){
// 对于TCP,需要先处理客户端发来的连接
// 通过读写clientSocket来和客户端进行通信
// 如果没有客户端发来连接,此时accept就会发生堵塞
// clientSocket每个客户端连接都会创建一个新的,每个客户端断开连接,这个对象也可以不要了,所以要关闭,以防文件资源泄露
Socket clientSocket = serverSocket.accept();
// 使用线程池
executorService.submit(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
// 处理一个客户端的连接
// 可能设计到多个客户端的连接和请求,这里暂时不写
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
// 获取输入流输出流对象
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream= clientSocket.getOutputStream()) {
// 对inputStream套了一层
Scanner scanner = new Scanner(inputStream);
// 对OutputStream套了一层
PrintWriter writer = new PrintWriter(outputStream);
while (true){
// 1.读取请求并解析 可以用read,也可以用Scanner,Scanner构造方法填入的是InputStream对象
// byte[] request = new byte[1024];
// inputStream.read(request);
if(!scanner.hasNext()){
// 连接断开了
System.out.printf("[%s:%d]客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
String request = scanner.next();
// 2.根据请求计算响应
String response = process(request);
// 3.返回响应到客户端
//outputStream.write(response.getBytes());
writer.println(response);
writer.flush();
// 打印日志
System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
修改后的运行结果如下
服务器端:

客户端1 :

客户端2:

学习路上一起进步,如果觉得内容不错,记得点赞支持一下,也可以关注我,后续持续分享高质量技术文章!