网络编程-TCP套接字

文章目录

初始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协议进行网络通信


服务器端

创建网卡以及构造方法

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

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

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

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

关于长短连接

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

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

处理每一个连接的代码逻辑

关键内容都在注释中了

java 复制代码
// 处理连接的方法, 这才是真正的进行请求与响应
    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一致, 都是汉译英的功能

java 复制代码
// 处理请求的主方法(翻译)
    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, "未收录该词条");
    }

客户端

创建网卡, 构造方法

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

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

java 复制代码
// 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()方法进行消息的推送

改进代码如下

java 复制代码
	// 2. 发送请求
	out.println(req);
    out.flush();

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

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

上述测试代码的逻辑中, 使用

java 复制代码
	out.println(req);

因为println天然的就带有一个换行, 所以这就成为了一个天然的分割条件

关于如果解决粘包问题, 我们之后会仔细说, 这里只是简单介绍一下...


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

分析下面的代码片段

假设有一台服务器, 此时客户端A尝试与服务器建立连接, 然后服务器进行连接的处理, 这时, 服务器就要阻塞等待in.hasNext()这里, 如果另一个客户端B也尝试和服务器建立连接, 那此时就不会有任何的反应(因为代码阻塞无法进行连接 ), 那岂不是一台服务器只能给一台客户端提供服务 ?

显然这样是不合理的, 所以此时我们就引入了多线程来解决这个问题, 通过不同的线程把处理连接的内容和接收连接的内容分隔开, 实质上这也就是早期发明多线程的原因(为了解决服务器开发的问题)

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


处理问题之后的完整代码

启动多个实例

首先要设置启动的时候运行启动多个实例这一项, 来模拟同时启动多个客户端

勾选Allow multiple instances


完整代码

客户端代码

java 复制代码
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();
    }
}

服务器端代码

java 复制代码
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多路复用机制的引入

相关推荐
laimaxgg5 分钟前
Linux网络连接内核
linux·运维·服务器·网络·网络协议·tcp/ip
小徐同学14181 小时前
BGP边界网关协议(Border Gateway Protocol)路由引入、路由反射器
运维·网络·网络协议·华为·智能路由器·信息与通信·bgp
APItesterCris4 小时前
如何监控和防范小红书笔记详情API的安全风险?
网络·笔记·安全
运维技术小记4 小时前
rhel7.9利用有网络环境打包ansible
网络·ansible
明朝百晓生4 小时前
【无线感知会议系列-21 】无线感知6G 研究愿景
网络·人工智能·算法·5g
iceman19524 小时前
TCP Window Full是怎么来的
服务器·网络·tcp/ip
别致的影分身4 小时前
Linux网络 TCP socket
linux·网络·tcp/ip
web150850966415 小时前
显卡(Graphics Processing Unit,GPU)架构详细解读
大数据·网络·架构
Andya_net6 小时前
计算机网络 | IP地址、子网掩码、网络地址、主机地址计算方式详解
网络·tcp/ip·计算机网络