TCP实现回显服务器及客户端

news2024/11/21 0:21:37

目录

前言:

Socket API

SeverSocket API

TCP中的长短连接

TCP实现回显服务器

代码实现(有详细解释)

TCP实现回显客户端

代码实现(有详细注释)

小结:


前言:

    上篇文章介绍了TCP的特点。由于TCP的特点是有连接,面向字节流,可靠传输等,我们就可以想象到TCP的代码和UDP会有一定的差异。TCP和UDP具体使用哪种协议需要根据实际业务需求来选择。

Socket API

    Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

    不管是客户端还是服务端Socket,都是双方建立连接后,保存两端信息,及用来与对方收发数据的。

Socket构造方法

注意:

    创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。当服务端accept()阻塞时,客户端一旦实例出Socket对象,就会建立连接。

Socket方法 

注意:

    获得套接字输入流。如果建立连接,服务端调用这个方法,就是读取客户端请求。

注意:

    获得套接字输出流。如果建立连接,服务端调用这个方法,就是往客户端返回响应。

注意:

    连接后获得对方的IP地址。

SeverSocket API

    ServerSocket 是创建TCP服务端Socket的API。

ServerSocket构造方法

    创建服务端套接字,并绑定端口。这个对象就是用来与客户端建立连接的。

ServerSocket方法

注意:

    开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,用来收发数据,否则阻塞等待。

 

注意:

    由于在操做系统中Socket被当作文件处理,那么就需要释放PCB中文件描述符表中的资源,同时断开连接。

TCP中的长短连接

    短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
    长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

注意:

    1)建立关闭连接耗时:很明显短连接需要不断的建立和断开连接,而长连接只需要一次。长连接耗时要比短连接短。

    2)主动发送请求不同:短连接一般是客户端主动向服务端发送请求。长连接客户端可以向服务端主动发送,服务端也可以主动向客户端发送。

    3)两者使用场景不同:短连接一般适用于客户端请求频率不高的场景(浏览网页)。长连接一般适用于客户端与服务端通信频繁的场景。(聊天室)

TCP实现回显服务器

    首先服务器是被动的一方,我们必须指定端口。然后通过ServerSocket对象中accept()方法建立连接,当返回Socket对象时,处理连接并且将响应写回客户端。

    由于不知道客户端什么时候建立连接,那么服务器就需要一直等待(随时待命)。这里使用了死循环的方式,但是不会一直循环,accept()方法当没有连接时就会阻塞等待。

    这里是本机到本机的数据发送,即使用环回ip即可。

 private ServerSocket serverSocket = null;
 public TcpEchoSever(int port) throws IOException {
     serverSocket = new ServerSocket(port);
 }

注意:

   创建ServerSocket对象,并且指定端口号。

Socket clintSocket = serverSocket.accept();

注意:

    accept()方法会阻塞等待。客户端Socket对象一旦实例化,就会与服务端建立连接。

 processConnection(clintSocket);

注意:

    这里通过一个方法来处理连接。这样写会有很大的好处。

 try(InputStream inputStream = clintSocket.getInputStream();
     OutputStream outputStream = clintSocket.getOutputStream())

注意:

    我们首先需要获得读和写的流对象。服务器需要接收请求(读),返回响应(写)。这里使用的是带有资源的try(),这样就会自动关闭流对象。

Scanner scanner = new Scanner(inputStream);
String request = scanner.next();

注意:

    这里通过Scanner去从流对象中读取数据。注意这里的next()方法,当读到一个换行符/空格/其他空白符结束,但最终结果不包含上述空白符。

    因为我们不清楚客户端连接后发送多少次请求,因此我们采用死循环的方式读和向客户端响应数据。这里不会一直循环因为scanner当读不到数据就会阻塞。

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

注意:

    这里通过一个函数来处理请求并且返回处理后结果。由于是回显服务器直接返回即可。

  PrintWriter printWriter = new PrintWriter(outputStream);
  printWriter.println(response);
  printWriter.flush();

注意:

    我们为了方便直接写字符串,将outputStream转换成PrintWriter。然后将响应写入到网卡,并且换行。因为客户端和服务端读数据都是需要空白符结束的,所以这里必须有一个空白符。

    由于数据首先会写入缓冲区,我们将缓冲区刷新一下保证数据正常写入到文件中(网卡)

finally {
      clintSocket.close();
}

注意:

    和一个客户端建立连接后,返回Socket对象(使用文件描述表),如果并发量大(会创建很多对象,文件描述符表就有可能满),就可能导致无法创建连接。因此需要保证资源得到释放,包裹在finally里。

特别注意:

    上述代码只能处理一个客户端。当代码执行到processConnection函数里,首先是一个死循环,然后还有scanner的阻塞,当处理一个连接代码就会一直在这个函数里。没有办法执行到accept()和客户端连接。想要处理下一个客户端的连接,就必须断开这个客户端,显然这是不合理的。

解决方案:

    使用多线程。当有客户端连接后,创建一个线程去处理这个连接,主线程代码继续执行,就会到accept()方法。要是有多个客户端都可以建立连接,并且有独立的线程去处理这些连接,这些线程是并发的关系。

    但是存在一个问题,如果并发量足够大(客户端数量非常多),就会创建大量的线程,也会存在大量线程的销毁,这些就会消耗大量的系统资源。因此使用线程池,使用动态变化的线程数量,根据并发量来调整线程数量。而且直接使用线程池中的线程代码上就可以实现,这样就会减少系统资源的消耗。

代码实现(有详细解释)

public class TcpEchoSever {
    //Tcp协议服务器,使用ServerSocket类,来建立连接
    private ServerSocket serverSocket = null;
    public TcpEchoSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        //使用线程池,防止客户端数量过多,创建销毁大量线程开销太大
        //动态变化的线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            //这里会阻塞,直到和客户端建立连接,返回Socket对象,来和客户端通信
            //客户端构造Socket对象时,会指定IP和端口,就会建立连接(客户端主动连接)
            Socket clintSocket = serverSocket.accept();
            threadPool.submit(() -> {
                try {
                    processConnection(clintSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            //要连接多个客户端,需要多线程去处理连接
            //这样才能让主线程继续执行到accept阻塞,然后和其他客户端建立连接(每个线程是独立的执行流,彼此之间是并发的关系)
            //如果客户端数量非常大,这里就会创建很多线程,数量过多对于系统来说也是很大的开销(使用线程池)
//            Thread t = new Thread(() -> {
//                try {
//                    processConnection(clintSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();
        }
    }

    private void processConnection(Socket clintSocket) throws IOException {
        System.out.printf("【%s : %d】客户端上线\n", clintSocket.getInetAddress(), clintSocket.getPort());
        //读客户端请求
        //处理请求
        //将结果写回客户端(响应)
        try(InputStream inputStream = clintSocket.getInputStream();
            OutputStream outputStream = clintSocket.getOutputStream()) {

            //流式数据,循环读取
            while (true) {
                Scanner scanner = new Scanner(inputStream);
                //读取完毕,客户端下线
                if(!scanner.hasNext()) {
                    System.out.printf("【%s : %d】客户端下线\n", clintSocket.getInetAddress(), clintSocket.getPort());
                    break;
                }
                //读取请求
                // 注意!! 此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符 .
                String request = scanner.next();
                //处理请求
                String response = process(request);

                //写回客户端处理请求结果(响应)
                //为了直接写字符串,这里将字节流转换为字符流
                //也可以将字符串转为字节数组
                PrintWriter printWriter = new PrintWriter(outputStream);
                //写入且换行
                printWriter.println(response);
                //写入首先是写入了缓冲区,这里为了保险就刷新一下缓冲区
                printWriter.flush();
                System.out.printf("【%s : %d】请求:%s  响应:%s\n", clintSocket.getInetAddress(), clintSocket.getPort(),
                        request, response);
            }

        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            //和一个客户端建立连接后,返回Socket对象(使用文件描述表),如果并发量大(会创建很多对象,文件描述符表就有可能满),就可能导致无法创建连接
            //因此需要保证资源得到释放,包裹在finally里
            clintSocket.close();
        }
    }
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoSever tcpEchoSever = new TcpEchoSever(8280);
        tcpEchoSever.start();
    }
}

TCP实现回显客户端

    客户端不需要指定端口号。客户端程序在用户主机上,我们如果指定就有可能和其他程序冲突,因此让操作系统随机分配一个空闲的端口号。客户端需要明确服务端的ip和端口号,这样才能明确哪个主机和哪个进程。

    那么服务端为什么可以指定端口号呢?难道就不怕和其他进程端口号冲突吗?(这里详解请看上篇文章的解释)

    首先需要明确客户端的工作流程:接收用户输入数据 --> 发送请求 --> 接收响应

public TcpEchoClint(String severIp, int severPort) throws IOException {
    socket = new Socket(severIp, severPort);
}

注意:

    创建Socket对象,并且指定服务端的ip和端口。当这个对象实例创建完成时,同时也就和服务端建立了连接,通过这个Socket对象就可以发送和接收数据。

    这里不需要将字符串ip进行转换,可以自动转换。

try(InputStream inputStream = socket.getInputStream();
    OutputStream outputStream = socket.getOutputStream())

注意:

    和服务端一样首先获得输入和输出流。用包含资源的try可以自动关闭,释放文件描述符表中的资源。

 System.out.println("请输入请求:");
 String request = scanner.next();
 if(request.equals("exit")) {
       System.out.println("bye bye");
       break;
  }

注意:

    让用户从控制台输入数据,这里做了一个判断,如果输入“exit”就退出客户端(break直接跳出循环)。

PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();

注意:

    为了直接发送字符串,这里将outputStream转换成PrintWriter。这里在发送时需要换行(空白符),因为服务端读取的next()方法需要空白符。

    数据首先写入缓冲区,为了保证数据写入到文件(网卡),这里手动刷新一下缓冲区。

Scanner scanner1 = new Scanner(inputStream);
String response = scanner1.next();
System.out.println(response);

注意:

    接收响应,通过输入流来读取响应。将接收的响应打印出来。这里的next()方法和上面一致。

代码实现(有详细注释)

public class TcpEchoClint {
    Socket socket = null;
    public TcpEchoClint(String severIp, int severPort) throws IOException {
        //Socket构造方法,可以识别点分十进制,不需要转换,比DatageamPacket方便
        //实例这个对象的同时,就会进行连接
        socket = new Socket(severIp, severPort);
    }
    public void start() {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                //从控制台读取请求
                //空白字符结束,但不会读空白字符
                System.out.println("请输入请求:");
                String request = scanner.next();
                if(request.equals("exit")) {
                    System.out.println("bye bye");
                    break;
                }
                //发送请求
                PrintWriter printWriter = new PrintWriter(outputStream);
                //需要发送空白符,因为scanner需要空白符
                printWriter.println(request);
                printWriter.flush();
                //接收响应
                Scanner scanner1 = new Scanner(inputStream);
                String response = scanner1.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClint tcpEchoClint = new TcpEchoClint("127.0.0.1", 8280);
        tcpEchoClint.start();
    }
}

小结:

    在写服务端代码时,需要考虑高并发的情况。我们需要尽可能节省系统资源的利用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

195136-58-4,2‘,7‘-Difluorofluorescein,2,7-二氟荧光素

产品描述:2,7-二氟荧光素中Fluorescein (Uranine) 生物应用中的荧光示踪剂,Fluorescein (Uranine) 是一种具有代表性的绿色荧光团,已被广泛用作实用绿色荧光探针的支架。结构式:理论分析:中文名&#xff1a…

1608_PC汇编语言_first例程分析

全部学习汇总: GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 这一次的笔记主要是梳理一个例程以及部分辅助信息。 为什么要学习汇编语言呢?现在的高级语言开发效率高,而且有着不错的效率,是…

Compose之跨平台Activity页面

前言 Compose(Jetpackjb)是可以跨平台的 目前跨平台主流的页面导航方式一般有两种 一种是都在同一个页面内进行页面替换类型的导航,类似于单Activity,多Fragment 另一种是不同的页面在不同的页面载体上,类似多Activity 两种页面导航方式都可以使用,但我比较偏向于使用多Ac…

加密签名算法

序言如果我们要消费的一段字符串没有唯一标识,我们该如何防止重复消费.cuiyaonan2000163.com背景信息加密加密技术是最常用的安全保密手段,利用技术手段把重要的数据变为乱码(加密)传送,到达目的地后再用相同或不同的手段还原&…

【虚拟仿真】Unity3D中实现鼠标悬浮模型上显示文字

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 在仿真项目开发中,会遇到让鼠标悬浮模型后显示模型…

《流浪地球2》MOSS出圈,现实中的量子计算机长啥样?

科技云报道原创。 春节档科幻电影《流浪地球2》上映后,影片中被誉为“全场最有价值道具”的智能量子计算机MOSS(代号550W)火了! 如果你看完了片尾彩蛋,会发现MOSS或许是整个流浪地球系列电影中的最大反派&#xff0c…

支持向量机-数学模型

支持向量机(support vector machines, SVM)是一种二分类模型。基本模型是定义在特征空间上的间隔最大的线性分类器;学习策略:间隔最大化;学习算法:求解凸二次规划的最优化算法。 支持向量机分为线性可分支…

自动驾驶感知——激光雷达基本概念|激光雷达点云|激光雷达的标定

文章目录1. 激光雷达基本概念1.1 激光雷达特点1.2 激光雷达测距原理1.2.1 系统组成1.2.2 激光雷达测距原理1.3 常见的激光雷达1.3.1 机械旋转式激光雷达1.3.2 Velodyne HDL-64E1.3.3 固态激光雷达1.3.4 Flash型固态激光雷达1.3.5 相控阵固态激光雷达1.3.6 MEMS型固态激光雷达1.…

NX二开ufun函数UF_MODL_create_surf_from_cloud(使用现有点创建B曲面)

本节主要讲使用现有点创建B曲面的ufun函数UF_MODL_create_surf_from_cloud,这些点可以按任何顺序出现,但是必须有足够的点来定义平面。 先看实例效果图: 1、函数结构 int UF_MODL_create_surf_from_cloud ( int point_cnt&#…

双栏Latex模板插入多个图片的各种排布

目录 问题描述: 问题解决: 1. 插入一个图片 示意图如下: 2.插入2x1两个图片:2x1 示意图如下: 3. 插入1x2两个图片:1x2 示意图如下: 4.插入2x2两个图片:2x2 示意图如下&…

shell脚本set -eu引起的学习笔记

本文将介绍Bash中 set -euxo pipefail 大佬的文章学习膜拜体验,它们可以帮助你写出更容易维护也更安全的脚本。这也是Bash脚本的终极调试手段,希望你以后在自己的脚本中加上这么一行,头顶也能少秃一点 今天打开一个脚本《JetsonNano使能SPI总…

00后真的是躺平的一代吗?已经被公司新来的00后卷哭了

2023年开年了,公司也来了许多新面孔,其中居然有一个是00年的,虽然也给公司带来了一些新的血液,但也让我意识到,自己年级确实不小了。这小老弟是去年毕业的,在某软件公司干了半年,现在跳槽到我们…

java常用类: String,StringBuffer和StringBuilder的底层区别

java常用类型: Ineteger等包装类 String类,StringBuffer类和StringBuilder类 Math类及常用方法 System类及常用方法 Arrays类及常用方法 BigInteger类和BigDecimal类及常用方法 日期类Date类,Calender类和LocalDateTime类 文章目录String介绍String创建细节String s…

大数据技术架构(组件)13——Hive:字符串函数

1.4.7、字符串函数1.4.7.1、asciiselect ascii(a);1.4.7.2、base64-->Hive0.12.0select base64(cast(abcd as binary));1.4.7.3、character_length-->Hive2.2.01.4.7.4、chr-->Hive1.3.01.4.7.5、concatselect concat(1,1,2,a),concat(1,a,null);1.4.7.6、context_ngr…

【JavaSE】ArrayList的扩容机制源码分析

文章目录1. ArrayList概述2. ArrayList构造方法源码分析3. ArrayList.add()源码分析4. ArrayList.addAll()源码分析5. 总结1. ArrayList概述 ArrayList是Java集合框架中比较常用的一个数据结构了,它底层是基于数组实现的。数组是固定大小的,但是ArrayLi…

禾川HCQ+X3E ModBUS 电机远程启动

前面讲过了 通过EtherCat总线级联X3E控制伺服电机,这次使用ModBus总线远程控制该电机启停。 硬件: HCQ0 1100/1200D X3EB 驱动 SV系列电机,主机电脑或者主PLC,硬件连接:电脑网口连HCQ0 port1 port2 连X3EB,软件需要用到TCP调试工具。 步骤一通讯: 1建立工程,修改本机地…

1月31日 : 读书笔记

为了让操作系统能够使用32位模式,需要对CPU做各种设定 最近的操作系统能同时运行多个程序,如果内存地址的使用范围重叠了怎么办?解决这个问题的方法就是分段。 什么是分段? 打个比方,将4GB的内存分成很多块&#xff0c…

【Mysql第五期 排序与分页】

文章目录案例使用的数据脚本1. 排序数据1.1 排序规则1.2 单列排序1.3 多列排序2.分页2.1 需求2.2 实现规则3.课后习题扩展分析原因问题解决总结案例使用的数据脚本 1.mysql脚本下载链接https://download.csdn.net/download/qq_43674360/87408079 2.或者自己新建一个sql后缀文本…

京东数据分析(竞品监控):飞利浦王牌产品在中国失利

近日,飞利浦集团发布了2022年第四季度及全年的业绩报告。根据报告显示,第四季度集团销售额达54亿欧元,可比销售额增长3%,可比订单量减少8%。 而全年业绩数据显示,集团销售额为178亿欧元,可比销售额下降3%&a…

sql进阶,多表及关联

–odps sql –– –author:宋文理 –create time:2023-02-01 16:24:24 –– – 创建非分区表 CREATE TABLE csxx_ffq( rq STRING COMMENT ‘日期’, xh BIGINT COMMENT ‘序号’, sj STRING COMMENT ‘数据’ ) COMMENT ‘测试数据(非分区表)’; – 创建分区表 CRE…