从网络基础到Socket编程:TCP/UDP原理 + Java实战详解

文章目录

一、网络基础概念

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 和端口号 portInetSocketAddressSocketAddress 的子类。

使用提示:构造 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:


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

相关推荐
2301_780789661 小时前
云服务器被黑能恢复吗?云服务器被黑的解决办法
运维·服务器·网络·安全·web安全
AI精钢1 小时前
修复 AI Gateway 图片 MIME 类型错误:用魔数检测替代扩展名猜测
网络·人工智能·python·gateway·aigc
我是无敌小恐龙2 小时前
Java基础入门Day10 | Object类、包装类、大数/日期类、冒泡排序与Arrays工具类 超详细总结
java·开发语言·数据结构·算法·贪心算法·排序算法·动态规划
极客先躯2 小时前
高级java每日一道面试题-2025年12月07日-实战篇[Dockerj]-Docker daemon 的配置文件在哪里?常用的配置项有哪些?
java·docker·配置文件的实际位置·配置文件的格式规则·常用配置项全景与分类·配置如何生效·daemon 配置折射架构思维
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【49】状态图运行时引擎:CompiledGraph 源码解析
java·人工智能·spring
Tutankaaa3 小时前
从10队到50队:知识竞赛软件的高并发场景如何设计?
java·经验分享·后端·spring
下次再写3 小时前
微服务架构实战:Spring Boot + Spring Cloud 从入门到精通
java·spring boot·spring cloud·微服务架构·服务注册与发现·分布式系统·api网关
bang冰冰3 小时前
Trae工具安装和使用教程(新手零基础入门,全程无坑)
java·人工智能·python
阿丰资源3 小时前
基于Spring Boot的网上摄影工作室系统(源码一键运行)
java·spring boot·后端