Socket学习

1.什么是Socket

1.1 定义

Socket(套接字)是计算机网络中进行通信的一种基本技术,它允许不同计算机上的程序通过网络进行数据交换。Socket本质上是一个通信端点,为网络通信提供了编程接口。

1.2 Socket的基本概念

Socket可以理解为网络通信的"插座",它为应用程序提供了与网络协议栈交互的编程接口。在网络通信中,Socket扮演着以下重要角色:

  1. 通信端点:每个Socket代表一个通信的端点,包含IP地址和端口号
  2. 数据传输通道:在建立连接后,Socket提供了双向的数据传输通道
  3. 协议抽象层:Socket抽象了底层网络协议的复杂性,为开发者提供统一接口

1.3 Socket的工作原理

Socket通信通常遵循客户端-服务器模型:

  1. 服务器端

    • 创建Socket并绑定到特定IP地址和端口
    • 监听连接请求
    • 接受客户端连接
    • 进行数据交换
  2. 客户端

    • 创建Socket
    • 连接到服务器指定的IP和端口
    • 进行数据交换

1.4 Socket的类型

主要有两种类型的Socket:

  1. 流式Socket(SOCK_STREAM)

    • 面向连接的可靠通信
    • 使用TCP协议
    • 保证数据按序到达且不丢失
    • 常用于Web浏览、文件传输等场景
  2. 数据报Socket(SOCK_DGRAM)

    • 无连接的不可靠通信
    • 使用UDP协议
    • 不保证数据到达顺序或是否到达
    • 常用于视频流、在线游戏等实时性要求高的场景

1.5 在OSI 七层网络模型的数据传输示意图socket的作用

这张图展示了两台设备间数据从应用层到物理层的传递过程并标明了socket在那一层起作用

1.5.1 层级与功能

  • 应用层:生成数据,提供 HTTP、DNS 等传输协议;
  • 表示层:对数据进行加密、压缩等处理;
  • 会话层(socket):负责建立设备间的通信连接;
  • 底层(传输层→物理层):由操作系统处理数据的分段、路由、编码等,最终通过物理硬件(网卡)完成传输。

1.5.2 核心作用

其功能是标准化数据在不同设备间的传输流程 :确保数据从生成、处理、连接建立到物理传输的每一步都有统一规则,实现跨设备、跨网络的可靠通信。Socket在会话层参与连接的建立。

socket负责端与端直接的连接:

当一个请求打过来的时候会通过网卡进行筛选匹配是否有符合端口号的请求。

2.如何创建socket实现客户端和服务器端的通信

Socket通信流程

  1. 服务器端创建Socket并绑定端口
  2. 服务器端监听连接请求
  3. 客户端创建Socket并连接服务器
  4. 建立连接后进行数据交换
  5. 通信完成后关闭连接
服务器:

创建一个服务器监听端口(4477),在java中封存了一个ServerSocket,创建对象进行端口注册;

需要一个线程经行执行。使用while(true)让主线程处于持续监听状态,其中:

java 复制代码
Socket socket = serverSocket.accept();

阻塞监听,让整个线程处于等待状态,直到客户端发来消息;如果客户端发送来消息才能进行往下执行。

java 复制代码
socket.getInputStream()

使用getInputStream()方法获取输入流

之后调用bufferedReader:

java 复制代码
  BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));

构建一个高效的 "字节→字符" 文本读取器,专门用于将 Socket 输入流(原始字节数据)转换为可直接操作的字符数据,读取数据,最后数据返回一开始阻塞监听状态。

java 复制代码
package com.socket;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 *服务器
 */
public class Server {
    // 服务器端键监听的端口
    public static final Integer SERVER_PORT = 4477;
    public static void main(String[] args) throws  Exception{
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接...");
        while (true){
            // 监听客户端连接
            Socket socket = serverSocket.accept();// 阻塞监听,直到有客户端连接
            InputStream inputStream = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String msg = br.readLine();
            System.out.println("收到客户端信息:"+msg);
        }
    }
}
客户端:

创建socket:

java 复制代码
Socket socket = new Socket("127.0.0.1", 4477);

第一个是ip这个ip是本机的意思,第二个是端口号。

将数据写入使用getoutputstream,打开写入,刷新。

java 复制代码
package com.socket;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

/*
*客户端
 */
public class Click {
    public static void main(String[] args) throws IOException {
        //http://localhost:4477/
        Socket socket = new Socket("127.0.0.1", 4477);
        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好服务器,我是客户端");
        printWriter.flush();
    }
}

第一步是先启动服务器,第二步是启动客户端。

3.引入子线程

main方法要求程序从上到下执行,程序会一直处于监听--处理--再监听状态。如果客户端比较多发送数据比较快,这个时候服务器就无法对其进行监听,可能会造成数据丢失。

3.1解决方法:

让子线程进行数据处理,主线程只负责监听

在主线程中创建子线程:

java 复制代码
new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler( socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

其中.start()方法不是开始是进入就绪队列等待cpu调度执行;让后将出了数据的过程写成一个方法直接调用。

java 复制代码
package com.socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 *服务器
 */
public class Server {
    // 服务器端键监听的端口
    public static final Integer SERVER_PORT = 4477;
    public static void main(String[] args) throws  Exception{
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接...");
        while (true){
            // 监听客户端连接
            Socket socket = serverSocket.accept();// 阻塞监听,直到有客户端连接
            //1.在主线程里面创建子线程
            //创建子线程处理数据
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler( socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();//.start()进入就绪队列等待cpu调度执行

        }
    }
    public static void  handler(Socket socket) throws IOException {
        //处理数据
        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String msg = br.readLine();
        System.out.println("收到客户端信息:"+msg);

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好客户端,我是服务器");
        printWriter.flush();
    }
}
java 复制代码
package com.socket;

import java.io.*;
import java.net.Socket;

/*
*客户端
 */
public class Click {
    public static void main(String[] args) throws IOException {
        //http://localhost:4477/
        Socket socket = new Socket("127.0.0.1", 4477);

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println("你好服务器,我是客户端");
        printWriter.flush();

        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String msg = br.readLine();
        System.out.println("收到服务器信息:"+msg);
    }
}

如果想要收到发送只需要添加一个:

java 复制代码
 Scanner sc = new Scanner(System.in);
        String sendMsg = sc.next();

重复之前的操作最后输出seedMsg.

java 复制代码
OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println(sendMsg);
        printWriter.flush();

3.2 开启子线程的方式有没有不好的地方

1.每次接受的客户端发来的数据就会创建一个新的线程;线程的创建和销毁都会消耗计算机的资源。

2.客户端的访问增多时,服务器和客户端的线程比是1:1;访问量大则线程增多,线程多可能会导致线程创建失败---->最终导致服务器宕机.

4.线程池

4.1 概括

客户端的访问案例增加时,服务端将呈现 1:1 的线程开销,访系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或僵死,从而不能对外提供服务接下来我们采用一个伪异步 I/O 的通讯框架,采用线程池和任务队列实现,当客户端接入时,将客户端的 Socket 封装成一个 Task (该任务实现 java.lang.Runnable 线程任务接口) 交给后端的线程池进行处理。jdk 的线程池维护一个消息队列和 N 个活跃的线程,对消息队列中 Socket 任务进行处理,由于线程池也可以设置消息队列的大小和最大线程数,因此他的资源占用是可控的,无论多少客户端并发访问,都不会导致资源的耗尽和宕机。

4.2 线程池里面的类容流程

线程池分为:核心线程和非核心线程

线程池里面两个参数:

corePoolsize:核心线程数,规定线程池有几个线程在运行。

maximumPoolsize:最大线程数。当workQueue满了,不能添加任务的时候,这个参数才会生效,规定线程池最多有几个线程在执行。

  1. keepAliveTime

    • 含义:超出**corePoolSize**(核心线程数)的线程的存活时间。若这些线程长时间无任务执行,且超过该时间,会被销毁。
    • 作用:控制非核心线程的资源占用,避免闲置线程浪费内存。
  2. unit

    • 含义:keepAliveTime的时间单位(如毫秒、秒)。
  3. workQueue

    • 含义:阻塞队列,用于存储待执行的任务。
    • 作用:当核心线程数已满时,新任务会被暂存到队列中,等待线程空闲后处理;队列的类型(如 LinkedBlockingQueue、SynchronousQueue)会影响线程池的任务调度策略。
  4. handler:当workQueue已经满了,并且线程池数maximumPoolsize,将执行拒绝策略。

线程池里面总会存在一批活跃的线程这类线程不用去创建可以及时使用。

4.3 工作原理图

客户端将消息发送过来首先要验证当前这个核心线程是否已满,如果没有直接那走一个核心线程去处理任务,如果满了就放入一个消息队列去排队;如果队列满了就创建一个其他线程去处理,如果没满就在消息队列里面等待;如何非核心线程也满了就直接按照拒绝策略进行处理。

拒绝策略:
策略名称 核心逻辑 特点与适用场景
AbortPolicy 丢弃新任务,并抛出RejectedExecutionException异常 直接暴露异常,适用于需明确感知任务拒绝的场景
DiscardPolicy 丢弃新任务,但不抛出异常 无感知丢弃任务,适用于任务非核心、丢失影响较小的场景
DiscardOldestPolicy 丢弃任务队列中最旧的任务,尝试重新提交新任务 会覆盖早期任务,适用于 "新任务优先级高于旧任务" 的场景
CallerRunsPolicy 由提交任务的线程(调用者线程)直接处理该任务 不会丢失数据,且能减缓任务提交速度,适用于异步任务批处理等需保证任务不丢失的场景

4.5 定义线程池

  1. 核心定位 :封装 JDK 原生 ThreadPoolExecutor,提供简洁的线程池创建与任务提交接口,专门适配 Socket 服务端的并发任务处理场景。

  2. 关键组成

    • 成员变量 executorService:底层实际工作的线程池对象(JDK ThreadPoolExecutor);
    • 构造方法:接收线程池核心配置参数(核心线程数、最大线程数、任务队列大小、空闲线程存活时间及单位),并初始化线程池(任务队列固定使用 ArrayBlockingQueue,基于数组的有界阻塞队列,避免队列无限制扩容);
    • execute(Runnable task) 方法:对外提供任务提交入口,将 Socket 连接封装的 Runnable 任务提交到线程池执行。
  3. 核心价值:通过线程池的 "核心线程 + 最大线程 + 有界队列" 机制,控制 Socket 服务端的并发资源占用,避免客户端高并发时因线程过多导致的内存溢出、进程宕机,提升服务端稳定性与并发处理能力。

java 复制代码
package com.socket;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/*
* 服务器处理线程池
 */
public class HandleSocketServerPool {
    //创建一个线程池成员变量,用于存储线程对象
    private ExecutorService executorService;//线程池对象,jdk提供的线程池对象
    /*
    *创建线程池
    * @param corePoolSize 核心线程数
    * @param maxThreadNum 最大线程数
    * @param queueSize 队列大小
    * @param keepAliveTime 线程空闲时间
    * @param unit 时间单位
     */
    public HandleSocketServerPool(Integer corePoolSize, int maxThreadNum, int queueSize, Integer keepAliveTime, TimeUnit unit){
        executorService = new ThreadPoolExecutor(corePoolSize, maxThreadNum, keepAliveTime, unit, new ArrayBlockingQueue<Runnable>(queueSize));
    }
    /**
     * 提交任务
     */
    public void execute(Runnable task){
        executorService.execute(task);
    }
}

4.6 使用

线程池提交的是任务,所有我们在使用线程池的时候要将socket对象封装成任务。

自定义一个ServerThreadReader类来封装socket对象。

java 复制代码
package com.socket;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

/**
 *服务器
 */
public class Server {
    // 服务器端键监听的端口
    public static final Integer SERVER_PORT = 4477;
    public static void main(String[] args) throws  Exception{
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("服务器启动成功,等待客户端连接...");
        //创建线程池
        HandleSocketServerPool handleSocketServerPool =
                new HandleSocketServerPool(5,10,10,120, TimeUnit.SECONDS);
        while (true){
            // 监听客户端连接
            Socket socket = serverSocket.accept();// 阻塞监听,直到有客户端连接
            Runnable task = new ServerThreadReader(socket);//将socket对象封装成任务
            handleSocketServerPool.execute(task);//提交任务,线程池会自动分配线程处理任务
        }
    }
}
java 复制代码
package com.socket;

import java.io.*;
import java.net.Socket;

/**
 * 将socket对象封装成任务
 */
public class ServerThreadReader extends Thread{
    private Socket socket;
    public ServerThreadReader(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        //处理数据

        try {
            InputStream inputStream = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String msg = br.readLine();
            System.out.println("收到客户端信息:"+msg);

            OutputStream outputStream = socket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.println("你好客户端,我是服务器");
            printWriter.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
java 复制代码
package com.socket;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/*
*客户端
 */
public class Click {
    public static void main(String[] args) throws IOException {
        //http://localhost:4477/
        Socket socket = new Socket("127.0.0.1", 4477);
        Scanner sc = new Scanner(System.in);
        String sendMsg = sc.next();

        OutputStream outputStream = socket.getOutputStream();
        PrintWriter printWriter = new PrintWriter(outputStream);
        printWriter.println(sendMsg);
        printWriter.flush();

        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String msg = br.readLine();
        System.out.println("收到服务器信息:"+msg);
    }
}
相关推荐
windfantasy19901 小时前
青少年编程考级:如何避免过度负担,让考级助力学习?
学习·青少年编程
香吧香2 小时前
Spring boot 中 CommandLineRunner 在服务启动完成后自定义执行
java·spring boot·spring cloud
浓墨染彩霞2 小时前
Java-----多线路
java·经验分享·笔记
清晓粼溪2 小时前
SpringMVC02:扩展知识
java·后端·spring
曹牧2 小时前
Java String[] 数组的 contains
java·开发语言·windows
qq_12498707532 小时前
基于springboot+vue+mysql的校园博客系统(源码+论文+部署+安装)
java·vue.js·spring boot·mysql·毕业设计
魂梦翩跹如雨2 小时前
P8752 [蓝桥杯 2021 省 B2] 特殊年份——Java解答
java·蓝桥杯
谷哥的小弟2 小时前
Spring Framework源码解析——Ordere
java·后端·spring·源码
浩瀚地学2 小时前
【Java】String
java·开发语言·经验分享·笔记·学习