当前位置: 首页 > article >正文

网络编程-TCP套接字

文章目录

  • 初始TCP套接字
  • TCP的Socket API
    • Socket
    • ServerSocket
  • 使用TCP模拟通信
    • 服务器端
    • 客户端
  • 上述测试代码的问题分析
    • IO的输入缓冲区的问题
    • 关于TCP协议中的粘包的问题
    • 不能进行多线程通信的问题
  • 处理问题之后的完整代码
    • 启动多个实例
    • 完整代码
    • 测试结果
  • 关于IO多路复用机制的引入

初始TCP套接字

我们在上一节简单的介绍了一下, 套接字的相关特性, 现在回顾一下
TCP的特性

  • 有连接: TCP套接字内部天然保存了对端的一些地址信息
  • 可靠传输: 尽最大的能力保障数据可以传输到指定位置
  • 面向字节流: 使用字节流的方式进行数据的传输, 大小没有限制, 但是可能存在一定的问题, 比如有可能出现粘包问题, 所以我们在传输的时候, 通常会使用一些特定的分隔符对数据的内容进行分割, 我们下面的测试代码也可以体现这一点
  • 全双工: 可以同时进行请求和响应

TCP的Socket API

和UDP相同, TCP也属于传输层的范畴, 所以在一台计算机内部, 就属于操作系统的管辖范围, 所以我们处在应用层的Java程序员, 就需要使用JVM提供的API接口进行编程(JVM也是封装了操作系统提供的一些API)


Socket

这个API主要是提供给客户端使用的, 当然服务器端也会使用, 是在服务器的ServerSocket对象调用accpet方法的时候作为返回值出现


常用的构造方法
在这里插入图片描述

  • 构造方法1: 创建一个套接字(未绑定对端地址)
  • 构造方法2: 创建一个套接字(绑定了对端的IP和端口号)
    注意, 该构造方法的IP可以直接传入字符串类型, 不用先转化为InetAddress类型然后传入(其实源码进行了一步转化操作)

常用方法

在这里插入图片描述

这个获取InputStream和OutputStream对象的方法, 可以说是最重要的方法, 因为我们的TCP是面向字节流传输的, 这个方法相当于提供了客户端和服务器端进行访问的通道…

在这里插入图片描述

和UDP类似, TCP的Socket操作的网卡资源, 也可以抽象为一种文件资源, 也占用文件操作符表的内存资源, 所以如果我们不用的话, 要及时的调用close方法进行资源的关闭…


ServerSocket

其实根据名字也不难看出, 这个ServerSocketAPI是专门给服务器端使用的, 客户端用的是另一套API, 等会再说


常用的两个构造方法

在这里插入图片描述

  • 构造方法1: 绑定一个随机的端口号(服务器不常用)
  • 构造方法2: 绑定一个固定的端口号(服务器常用的)

常用方法
在这里插入图片描述
上文我们说了, 在服务器的ServerSocketAPI的使用过程中, 也有Socket出现的时候, 正是当ServerSocket想要和客户端的Socket对象建立通信的时候, 本质上是调用accpet方法, 返回一个Socket对象, 然后使用该对象和客户端的Socket对象, 通过打开的IO通道进行通信

在这里插入图片描述
不再多说, 因为这也是一种文件的资源, 所以当我们不用的时候, 要进行及时的关闭, 避免占用文件操作符表的资源, 但是在真实的服务器的场景中, 使用这个方法的场景是有限的, 因为一般都是 7 * 24 小时持续运行

使用TCP模拟通信

下面我们简单写一个翻译的服务器, 来模拟一下使用TCP协议进行网络通信


服务器端

创建网卡以及构造方法

// 创建一个网卡Socket对象
    ServerSocket serverSocket = null;
    
    // 构造方法(传入一个固定的端口号作为服务器固定端口号)
    public TcpServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    

start方法进行和客户端通信的主流程

// start方法, 与客户端进行通信的主流程
    public void start() throws IOException {
        // 输出日志信息, 服务器上线
        System.out.println("服务器已上线...");
        // 使用while循环不断处理客户端发来的连接
        while(true){
            Socket connection = serverSocket.accept();
            // 处理这个连接
            processConnection(connection);
        }
    }

注意accept方法, 仅仅相当于客户端和服务器端之间建立了连接, 还没有进行请求和响应的内容…(类比)其实就相当于客户端和服务器端进行打电话, 只是拨通了, 还没开始说话

关于长短连接

上面我们说了, 建立连接之后, 才可以进行请求和响应, 那么这个连接中, 是包含一次请求响应还是多次请求响应的 ? 是客户端给服务器端发请求还是服务器端给客户端发请求 ? 这个都是说不准的, 要看具体的使用场景, 所以分为长短连接

  • 短连接: 一次连接只有一次请求响应, 比较消耗资源, 通常应用在查看网页, 内容展示等场景
  • 长连接: 一次连接中有多次请求响应, 比较节约资源, 且不仅仅可能是服务器端给客户端发请求, 服务器端也可能给客户端发请求, 通常应用在游戏, 在线聊天等场景

处理每一个连接的代码逻辑
关键内容都在注释中了

// 处理连接的方法, 这才是真正的进行请求与响应
    private void processConnection(Socket connection){
        // 输出日志, 表示客户端上线
        System.out.printf("客户端上线[%s:%d]", connection.getInetAddress().toString(), connection.getPort());
        // 打开connection的IO通道和客户端的联通(使用try-with-resource机制自动释放资源)
        try(InputStream inputStream = connection.getInputStream(); OutputStream outputStream = connection.getOutputStream()) {
            // 为了便于我们使用 IO, 我们对上面的输入输出流进行套壳处理(也可以不用)
            Scanner in = new Scanner(inputStream);
            PrintWriter out = new PrintWriter(outputStream);
            // 使用while循环不断尝试读取请求和响应
            while(true){
                // 1. 读取请求
                if(!in.hasNext()){
                    // 如果没有下一条请求了, 输出日志, 直接退出
                    System.out.printf("客户端下线[%s:%d]", connection.getInetAddress().toString(), connection.getPort());
                    break;
                }
                String req = in.next();
                
                // 2. 处理请求
                String resp = process(req);
                
                // 3. 发送请求
                out.println(resp);
                
                // 4. 记录日志
                System.out.printf("[req: %s, resp: %s]", req, resp);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

对请求进行处理的主函数, 和上次UDP一致, 都是汉译英的功能

// 处理请求的主方法(翻译)
    private static Map<String, String> chineseToEnglish = new HashMap<>();

    static {
        chineseToEnglish.put("小猫", "cat");
        chineseToEnglish.put("小狗", "dog");
        chineseToEnglish.put("小鹿", "fawn");
        chineseToEnglish.put("小鸟", "bird");
    }

    private String process(String req) {
        return chineseToEnglish.getOrDefault(req, "未收录该词条");
    }


客户端

创建网卡, 构造方法

// 创建网卡
    private Socket clientSocket = null;
    
    // 构造方法, 由于是Tcp协议, 所以直接绑定对端的地址信息
    public TcpClient(String host, int port) throws IOException {
        clientSocket = new Socket(host, port);
    }

start方法, 和服务器端的一些内容是相似的

// start方法, 与服务器建立通信, 请求与响应
    public void start(){
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream(); Scanner sc = new Scanner(System.in)) {
            // 把输入输出流进行包装
            Scanner in = new Scanner(inputStream);
            PrintWriter out = new PrintWriter(outputStream);
            
            // 使用while循环不断读取用户的请求, 发送请求并接收响应
            while(sc.hasNext()){
                // 1. 读取请求
                String req = sc.next();
                
                // 2. 发送请求
                out.println(req);
                
                // 3. 接收响应
                String resp = in.next();
                
                // 4. 输出响应内容
                System.out.println(resp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

上述测试代码的问题分析

如果我们直接运行上面的代码, 我们就会发现, 是无法直接运行的, 说明上面的代码存在一些问题, 我们现在处理一下这些问题

IO的输入缓冲区的问题

有一些 IO 工具, 在输出的时候, 并不会是真正的输出, 而是将输出的内容放到一个缓冲区的地方, 必须调用flush()方法才能够真正的进行数据的发送, 所以我们在 IO 那个章节的时候, 建议是所有输出的流, 我们都进行flush()方法进行推送, 这是一个非常好的习惯, 所以上面的测试代码, 我们把所有使用out.println()的位置后面, 都加上flush()方法进行消息的推送

改进代码如下

	// 2. 发送请求
	out.println(req);
    out.flush();

关于TCP协议中的粘包的问题

由于TCP协议传输的时候, 是通过字节流的方式进行传输的, 所以不同的消息之间, 并没有一个非常明显的界限, 所以我们一般手动进行消息边界的指定, 避免消息的"粘连问题"
上述测试代码的逻辑中, 使用

	out.println(req);

因为println天然的就带有一个换行, 所以这就成为了一个天然的分割条件
关于如果解决粘包问题, 我们之后会仔细说, 这里只是简单介绍一下…


不能进行多线程通信的问题

分析下面的代码片段
在这里插入图片描述
在这里插入图片描述
假设有一台服务器, 此时客户端A尝试与服务器建立连接, 然后服务器进行连接的处理, 这时, 服务器就要阻塞等待in.hasNext()这里, 如果另一个客户端B也尝试和服务器建立连接, 那此时就不会有任何的反应(因为代码阻塞无法进行连接), 那岂不是一台服务器只能给一台客户端提供服务 ?
显然这样是不合理的, 所以此时我们就引入了多线程来解决这个问题, 通过不同的线程把处理连接的内容和接收连接的内容分隔开, 实质上这也就是早期发明多线程的原因(为了解决服务器开发的问题)

为了不频繁的创建销毁线程导致资源开销太大, 我们又引入了线程池来解决这个问题


处理问题之后的完整代码

启动多个实例

首先要设置启动的时候运行启动多个实例这一项, 来模拟同时启动多个客户端
在这里插入图片描述
勾选Allow multiple instances


完整代码

客户端代码

package net_demo1.net_demo04;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * 关于使用Tcp协议的客户端程序的模拟
 */

public class TcpClient {

    // 创建网卡
    private Socket clientSocket = null;

    // 构造方法, 由于是Tcp协议, 所以直接绑定对端的地址信息
    public TcpClient(String host, int port) throws IOException {
        clientSocket = new Socket(host, port);
    }

    // start方法, 与服务器建立通信, 请求与响应
    public void start(){
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream(); Scanner sc = new Scanner(System.in)) {
            // 把输入输出流进行包装
            Scanner in = new Scanner(inputStream);
            PrintWriter out = new PrintWriter(outputStream);

            // 使用while循环不断读取用户的请求, 发送请求并接收响应
            while(sc.hasNext()){
                // 1. 读取请求
                String req = sc.next();

                // 2. 发送请求
                out.println(req);
                out.flush();

                // 3. 接收响应
                String resp = in.next();

                // 4. 输出响应内容
                System.out.println(resp);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) throws IOException {
        TcpClient tcpClient = new TcpClient("127.0.0.1", 9090);
        tcpClient.start();
    }
}

服务器端代码

package net_demo1.net_demo04;

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.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 使用TCP协议模拟的服务器
 */

public class TcpServer {

    // 创建一个网卡Socket对象
    ServerSocket serverSocket = null;

    // 构造方法(传入一个固定的端口号作为服务器固定端口号)
    public TcpServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    // start方法, 与客户端进行通信的主流程
    public void start() throws IOException {
        // 创建一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 输出日志信息, 服务器上线
        System.out.println("服务器已上线...");
        // 使用while循环不断处理客户端发来的连接
        while (true) {

            Socket connection = serverSocket.accept();

            executorService.execute(() -> {
                // 处理这个连接
                processConnection(connection);
            });
        }
    }


    // 处理连接的方法, 这才是真正的进行请求与响应
    private void processConnection(Socket connection) {
        // 输出日志, 表示客户端上线
        System.out.printf("客户端上线[%s:%d]\n", connection.getInetAddress().toString(), connection.getPort());
        // 打开connection的IO通道和客户端的联通(使用try-with-resource机制自动释放资源)
        try (InputStream inputStream = connection.getInputStream(); OutputStream outputStream = connection.getOutputStream()) {
            // 为了便于我们使用 IO, 我们对上面的输入输出流进行套壳处理(也可以不用)
            Scanner in = new Scanner(inputStream);
            PrintWriter out = new PrintWriter(outputStream);
            // 使用while循环不断尝试读取请求和响应
            while (true) {
                // 1. 读取请求
                if (!in.hasNext()) {
                    // 如果没有下一条请求了, 输出日志, 直接退出
                    System.out.printf("客户端下线[%s:%d]\n", connection.getInetAddress().toString(), connection.getPort());
                    break;
                }
                String req = in.next();

                // 2. 处理请求
                String resp = process(req);

                // 3. 发送请求
                out.println(resp);
                out.flush();

                // 4. 记录日志
                System.out.printf("[req: %s, resp: %s]\n", req, resp);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    // 处理请求的主方法(翻译)
    private static Map<String, String> chineseToEnglish = new HashMap<>();

    static {
        chineseToEnglish.put("小猫", "cat");
        chineseToEnglish.put("小狗", "dog");
        chineseToEnglish.put("小鹿", "fawn");
        chineseToEnglish.put("小鸟", "bird");
    }

    private String process(String req) {
        return chineseToEnglish.getOrDefault(req, "未收录该词条");
    }

    public static void main(String[] args) throws IOException {
        TcpServer tcpServer = new TcpServer(9090);
        tcpServer.start();
    }
}


测试结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


关于IO多路复用机制的引入


http://www.kler.cn/a/509475.html

相关文章:

  • 生成树机制实验
  • 计算机网络 (47)应用进程跨越网络的通信
  • vue编写一个可拖动的模块,并可以和任何其他组件组合使用
  • VSCode 的部署
  • Redis超详细入门教程(基础篇)
  • SQL-杂记1
  • 基于Spring Cloud的电商系统设计与实现——用户与商品模块的研究(下)
  • 数据库存储上下标符号,sqlserver 2008r2,dm8
  • 如何通过 Apache Airflow 将数据导入 Elasticsearch
  • 4.若依 BaseController
  • Gin 源码概览 - 路由
  • Android笔记: 实现点击事件透传到底部
  • Django缓存系统详解:使用Redis提升应用性能
  • 快手极速版如何查找ip归属地?怎么关掉
  • PP-OCR系统
  • CloudberryDB(四)并行执行
  • 【Linux系统编程】—— 深入理解Linux中的环境变量与程序地址空间
  • 电路笔记(通信模块): 基于ESP32-S3实现的XVC(Xilinx Virtual Cable) JTAG下载器硬件
  • 【LOJ 6198】谢特 题解(可持久化Trie+后缀数组SA+启发式分裂+RMQ)
  • Jenkins-git配置说明!
  • Android SystemUI——CarSystemBar添加到窗口(十)
  • Debian终端高亮(显示不同颜色)
  • JVM加载
  • Social LSTM:Human Trajectory Prediction in Crowded Spaces | 文献翻译
  • 学生信息管理系统数据库设计(sql server)
  • 【three.js】纹理贴图