初识TCP(编写回显服务器)

目录

  • 初识TCP(编写回显服务器)
    • TCP相关的API
    • 服务器代码实现
    • 客户端代码实现
    • 部分代码解释
    • 注意事项
    • 效果展示

初识TCP(编写回显服务器)

TCP相关的API

ServerSocket : 这是socket类,对应到网卡,但是这个类只能给服务器进行使用

socket : 对应到网卡,既可以给服务器使用,又可以给客户端使用

TCP是面向字节流的,传输的基本单位是字节

我们用一个回显服务器来演示TCP的工作过程。所谓回显服务器就是客户端发起什么请求,就返回什么响应。

服务器代码实现

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


public class TcpEchoServer {
    private ServerSocket serverSocket=null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket= new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            // 通过 accept 方法来 "接听电话", 然后才能进行通信
            Socket clientSocket = serverSocket.accept();
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }
    public 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()
        ) {
            while(true){
                Scanner scanner=new Scanner(inputStream);
                if(!scanner.hasNext()){
                    // 读取完毕. 客户端断开连接, 就会产生读取完毕.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 1. 读取请求并解析. 这里注意隐藏的约定. next 读的时候要读到空白符才会结束.
                //    因此就要求客户端发来的请求必须带有空白符结尾. 比如 \n 或者空格.
                String request=scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应返回给客户端
                //    通过这种方式可以写回, 但是这种方式不方便给返回的响应中添加 \n
                // outputStream.write(response.getBytes(), 0, response.getBytes().length);
                //    也可以给 outputStream 套上一层, 完成更方便的写入.
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();

                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);


            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }

    }
    public String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server=new TcpEchoServer(9090);
        server.start();

    }
}

客户端代码实现

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


public class TcpEchoClient {
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        // 此处可以把这里的 ip 和 port 直接传给 socket 对象.
        // 由于 tcp 是有连接的. 因此 socket 里面就会保存好这俩信息.
        // 因此此处 TcpEchoClient 类就不必保存.
        socket=new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("客户端启动!!");
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()
        ) {
            Scanner scannerConsole=new Scanner(System.in);
            Scanner scannerNetwork=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);
            while (true){
                // 这里的流程和 UDP 的客户端类似.
                // 1. 从控制台读取输入的字符串
                System.out.println("->");
                if(!scannerConsole.hasNext()){
                    break;
                }
                String request=scannerConsole.next();
                //2.把请求发给服务器,这里需要使用println来发送,为了让发送的请求末尾带有\n
                //这里是和服务器的scanner.next呼应的
                writer.println(request);
                writer.flush();
                //3.从服务器读取响应,这里也是和服务器返回响应的逻辑对应
                String response=scannerNetwork.next();
                //4.把响应显示出来
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

    }
}

部分代码解释

1.TCP是有连接的

和打电话一样,需要客户端拨号,服务器来接听: serverSocket.accept()

accept也是一个可能会产生阻塞的操作。如果当前没有客户端连过来,此时accept就会阻塞

2.为何这里要设置两个socket?

在这里插入图片描述

想象一个场景:你要去买房

ServerSocket

把服务器想象成售楼处,ServerSocket 就是售楼处里负责等购房者上门的工作人员。先确定好办公地点(端口号),然后通过accept()等购房者来。购房者一来,就安排出一个专门的购房洽谈室(Socket),用于后续沟通交流。

Socket

Socket 就如同那个购房洽谈室,是服务器(售楼处)和客户端(购房者)交流互动的专属空间,双方在这里收发信息,像在洽谈室里商量买房的各种事儿一样,完成数据通信。

每个客户端都会分配一个洽谈室。

这样的优点是:

分工明确,支持多客户端访问

3.TCP有连接

TCP socket中就会保存对端的信息

在这里插入图片描述

4.这里 的操作相当于把字节流转换成字符流

Java中,字节流(如InputStream和OutputStream)是以字节为单位来处理数据的。字符流(如Reader和Writer)是以字符为单位来处理数据的。

这里的PrintWriter是继承于Writer。转换一下方便后续操作

在这里插入图片描述
5.断开连接

在这里插入图片描述

注意事项

1.客户端输入之后,服务器没有响应

在这里插入图片描述

之所以出现上述的情况,本质原因在于PrintWriter内置的缓冲区在作祟。

为什么要设置缓冲区呢?因为IO操作都是比较低效的操作,就希望能够让低效操作,进行的尽量少一些。

因此引入缓冲区(内存),先把要写入网卡的数据放到内存缓冲区中,等到攒一波之后再统一进行发送(把多次IO合并成一次了)

但是也有个问题,如果发送的数据很少,此时由于缓冲区还没满,数据就待在缓冲区里,没有被真正被发送出去

【解决方法】手动刷新缓冲区,flush刷新缓冲区

在这里插入图片描述

在这里插入图片描述

2.上述代码需要进行close吗?

serverSocket不需要,socket需要

serverSocket整个程序只有唯一一个对象,并且这个对象的生命周期很长是要跟随整个程序的,这个对象无法提前关闭,只要程序退出,随着进程的销毁一起被释放即可(不需要手动进行)

但是TCP的client socket是每个客户端都有一个,随着客户端越来越多,这里消耗的socket也会越来越多(如果不加释放,就很可能把文件描述符表给占满)

【调用socket.close本质上也是关闭文件,释放文件描述符表,这里进程销毁,文件描述符表就没了】

在这里插入图片描述

3.当多个客户端来同时访问服务器

两个客户端同时发起请求,服务器只给第一个客户端响应,停止第一个客户端后,服务器才会给第二个客户端响应!这是肯定不行的。

服务器服务多个客户端是天经地义的!!

在这里插入图片描述

【问题分析】第一个客户端连上服务器之后,服务器就会从accept这里返回(解除阻塞)进入到processConnection中了。接下来就会在scanner.hasNext这里阻塞,等待客户端的请求。客户端请求到达之后,sanner.hasNext返回,继续执行,读取请求,根据请求计算响应,返回响应给客户端…执行完上述一轮操作之后,循环回来继续再hasNext阻塞,等待下一个请求。直到客户端退出之后,连接结束,此时循环才会退出。

虽然第二个客户端和服务器在内核层面上建立了TCP连接了,但是应用程序这里无法把连接拿到应用程序里处理
(人家给你打电话,你手机一直在响,但是你就是没接)

那么问题来了:第一个客户端退出了,第二个客户端之前发的请求啥的咋就立即被处理,而没被丢掉呢??

这是因为当前TCP在内核中,每个socket都是有缓冲区的

客户端发送的数据确实是发了,服务器也收到了,只不过数据是在服务器的接收缓冲区中

一旦第一个客户端退出了,回到第一层循环,继续执行第二次accept,继续执行next就能把之前缓冲区的内容读出来

【解决方法】核心思路是使用多线程,单个线程无法既能给客户端循环提供服务,又能去快速调用到第二次accept

简单的办法就是引入多线程

主线程就负责执行accept,每次有一个客户端连上来,就分配一个新的线程,由新的线程负责给客户端提供服务。

在这里插入图片描述

在这里插入图片描述

上述问题,不是TCP引起的,而是代码没写好,两层循环嵌套引起的

UDP服务器,只有一层循环,就不涉及到这样的问题,之前UDP服务器天然就可以处理多个客户端的请求

4.如果客户端比较多,就会使服务器频繁创建销毁线程

线程池就能解决频繁创建销毁的问题

5.如果当前的场景是线程频繁创建,但是不销毁呢

【解决方案】

1.引入协程

轻量级线程,本质上还是一个线程,用户态可以通过手动调度的方法让一个线程“并发”的做多个任务(省去系统调动的开销了)

2.IO多路复用

系统内核级别的机制,本质上是让一个线程同时去负责处理多个socket(因为这些socket数据并非时同一时刻都需要处理)

效果展示

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/929305.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kali Linux使用Netdiscover工具的详细教程

Kali Linux使用Netdiscover工具的详细教程 引言 在网络安全和渗透测试的过程中,网络发现是一个至关重要的步骤。Netdiscover是Kali Linux中一个非常实用的网络发现工具,它可以帮助用户快速识别局域网中的活动设备。本文将详细介绍如何使用Netdiscover工…

R语言机器学习论文(二):数据准备

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据一、数据描述二、数据预处理(一)修改元素名称(二)剔除无关变量(三)缺失值检查(四)重复值检查(五)异常值检查三、描述性统计(一)连续变量数据情…

Net9 Abp Vnext查询、高级搜索、过滤终极解决方案,ORM支持Freesql/SqlSugar/EFCore或原生sql

以员工管理表为例,常用栏位如下图 基本需求:默认搜索框可以模糊查询搜索工号、姓名、手机号、年龄等不需要关联查询基本字段。 特殊需求需要高级搜索:例如按入职区间、部门、公司、年龄段、上级主管等进行模糊搜索,且支持并且或者…

在办公室环境中用HMD替代传统显示器的优势

VR头戴式显示器(HMD)是进入虚拟现实环境的一把钥匙,拥有HMD的您将能够在虚拟现实世界中尽情探索未知领域,正如如今的互联网一样,虚拟现实环境能够为您提供现实中无法实现的或不可能实现的事。随着技术的不断进步&#…

PPT怎样做的更加精美

目录 PPT怎样做的更加精美 3D的GIF图片 3维空间图​编辑 结果有明显的对比 阅读高质量文献,采用他们的图 PPT怎样做的更加精美 3D的GIF图片 3维空间图 结果有明显的对比

Altium Designer学习笔记 26-27 PCB布局优化_规则创建

基于Altium Designer 23学习版,四层板智能小车PCB 更多AD学习笔记:Altium Designer学习笔记 1-5 工程创建_元件库创建Altium Designer学习笔记 6-10 异性元件库创建_原理图绘制Altium Designer学习笔记 11-15 原理图的封装 编译 检查 _PCB封装库的创建Al…

在Unity编辑模式下运行Mono中的方法

[ExecuteAlways] 最简单的方法当然是直接给Mono加上[ExecuteAlways]修饰,这样Mono中的Awake,Update等等都可以在编辑模式下按照原本的时机运行。 [ExecuteAlways] public class TestScript : MonoBehaviour {void TestMethod(){Debug.Log("TestMe…

PDF与PDF/A的区别及如何使用Python实现它们之间的相互转换

目录 概述 PDF/A 是什么?与 PDF 有何不同? 用于实现 PDF 与 PDF/A 相互转换的 Python 库 Python 实现 PDF 转 PDF/A 将 PDF 转换为 PDF/A-1a 将 PDF 转换为 PDF/A-1b 将 PDF 转换为 PDF/A-2a 将 PDF 转换为 PDF/A-2b 将 PDF 转换为 PDF/A-3a 将…

【超图】iClient3D for Cesium 以动静结合方式加载WMTS服务

作者:taco 一、问题来源 在最近支持的项目中,我们面临一个挑战:客户需要在前端动态加载高达3亿级别的白模底面数据。这样做的主要原因是客户的数据库会频繁更新,因此我们需要采用动态加载的方式来确保用户界面能够实时反映最新的…

Y20030026 VUE+Springboot+MYSQL+LW+实体店推广平台的设计与实现 源代码 配置 文档 PPT

实体店推广平台的设计与实现 1.摘要2.开发目的和意义3.系统功能设计4.系统界面截图5.源码获取 1.摘要 随着互联网的普及和电子商务的快速发展,消费者的购物习惯发生了显著变化。越来越多的消费者倾向于在线购物,享受便捷、丰富的选择和个性化的购物体验…

基数排序(代码+注释)

#include <stdio.h> #include <stdlib.h>// 获取数组中的最大值 int GetMax(int* a, int n) {int max a[0];for (int i 1; i < n; i) {if (a[i] > max) {max a[i];}}return max; }// 对数组按照某个位数进行计数排序 void CountingSortForRadix(int* a, i…

esp8266 编译、烧录环境搭建

一、准备 xtensa-lx106-elf-gcc8-4-0-esp-2020r3-win32下载&#xff1a;点击跳转 MSYS2 压缩包文件&#xff1a; 固件烧录工具&#xff1a;点击跳转 esp8266源码地址&#xff1a;点击跳转 二、搭建编译环境 1、在D盘创建一个ESP8266目录&#xff0c;解压MSYS2.zip到里面&a…

【Delphi】modbus-TCP 协议库

在日常开发中&#xff0c;也会遇到使用modbus的部件&#xff0c;比如温度控制器、读卡器等等&#xff0c;那么使用Delphi开发&#xff0c;也就必须遵守modbus-TCP协议&#xff0c;如果自己使用TCP控件写也没有问题&#xff0c;不过如果有开源的三方库&#xff0c;别人已经调试过…

AgGrid 组件封装设计笔记:自定义 icon 以及每个 icon 的点击事件处理

文章目录 问题目前解决效果 v1思路 目前解决效果 v0思路 代码V1 问题 自己封装的 AgGrid 如何自定义传递 icon &#xff0c;以及点击事件的处理&#xff1f; 目前解决效果 v1 思路 目前解决效果 v0 思路 一张图片说明一下 代码 V1 父组件使用 <template><MyPageL…

MySQL——MySQL 日志

文章目录 MySQL 文件介绍二进制日志&#xff08;bin log&#xff09;概念binlog 日志的三种格式两阶段提交binlog 落盘更新语句执行流程 慢查询日志&#xff08;slow query log&#xff09;重做日志&#xff08;redo log&#xff09;redo log 日志的理解WAL 技术redo log 的工作…

解决git did not exit cleanly (exit code 128)问题

解决 git did not exit cleanly &#xff08;exit code 128&#xff09;问题 1、错误描述2、解决方法2.1 方法一2.2 方法二 1、错误描述 使用TortoiseGit进行操作时&#xff0c;总是提示下述错误。 2、解决方法 2.1 方法一 打开 TortoiseGit -> Settings 点击 Network&…

家校通小程序实战教程05教师登录

目录 1 搭建角色选择页面2 设置登录方法3 创建加入班级页面4 创建教师主页页面5 完善登录方法总结 我们上一篇开发了教师管理的后台功能&#xff0c;后台一般是给管理员提供的。教师一般是使用小程序开展各类业务&#xff0c;本篇介绍一下教师如何通过小程序登录。 1 搭建角色选…

yolov5 解决:export GIT_PYTHON_REFRESH=quiet

当我们在第一次运行YOLOv5中的train.py程序时&#xff1a;可能会出现以下报错&#xff1a; This initial warning can be silenced or aggravated in the future by setting the $GIT_PYTHON_REFRESH environment variable. Use one of the following values: - quiet|q|silen…

neo4j如何存储关于liquidity structure的层次和关联结构

在 Neo4j 中存储关于流动性结构&#xff08;liquidity structure&#xff09;的层次和关联结构非常适合&#xff0c;因为 Neo4j 是一个基于图的数据库&#xff0c;能够自然地建模和存储复杂的关系和层次结构。下面是如何在 Neo4j 中设计和实现这样的数据模型的详细步骤和示例。…

SpringBoot高级-底层原理

目录 1 SpringBoot自动化配置原理 01-SpringBoot2高级-starter依赖管理机制 02-SpringBoot2高级-自动化配置初体验 03-SpringBoot2高级-底层原理-Configuration配置注解 04-SpringBoot2高级-底层原理-Import注解使用1 05-SpringBoot2高级-底层原理-Import注解使用2 06-S…