网络编程套接字(4)——Java套接字(TCP协议)

news2024/12/26 14:54:17

目录

一、Java流套接字通信模型

二、TCP流套接字编程 

1、ServerSocket

ServerSocket构造方法:

ServerSocket方法:

2、Socket

Socket构造方法:

Socket方法:

三、代码示例:回显服务器

1、服务器代码

代码解析

2、客户端代码

代码解析

3、注意事项

        (1)缓冲区

        (2)socket的close,释放文件描述符表

        (3)多线程的应用

        (4)引入线程池的改进

                1、协程

                2、IO多路复用

4、执行代码

        前述:

5、客户端和服务器交互的过程


一、Java流套接字通信模型

        

        1.客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但是真实的场景,一般是不同主机。

        2.注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程。

        3.Socket编程我们是使用流套接字和数据报套接字,基于TCP或UDP协议,但应用层协议,也需要考虑。


二、TCP流套接字编程 

        TCP面向字节流,和UDP面向数据报不同,但是写的回显服务器中心思想是一样的,代码会有不同。以下API介绍。

1、ServerSocket

        这个Socket类对应到网卡,只能给服务器使用。

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

ServerSocket构造方法:

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket方法:

方法签名方法说明
Socket accept()

开始监听指定端口(创建时绑定的端口),有客户端

连接后,返回一个服务端socket对象,并基于该

Socket建立与客户端的连接,否则阻塞等待

void close()关闭此套接字

2、Socket

        对应到网卡,既可以给客户端使用,也可以给服务器使用。

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

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

Socket构造方法:

方法签名方法说明
Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机

上,对应端口的进程进行连接

Socket方法:

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

三、代码示例:回显服务器

1、服务器代码

public class TcpEchoServer {
    ServerSocket socket = null;

    public TcpEchoServer(int serverPort) throws IOException {
        socket = new ServerSocket(serverPort);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            //通过 accept 这个方法来 “接听电话”,然后才能通信
            Socket clientSocket = socket.accept();
//            Thread t = new Thread(() -> {
//                //通过这个方法来处理一次连接,连接的过程会涉及到多次请求响应交互
//                processConnection(clientSocket);
//            });
//            t.start();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }
    //通过这个方法来处理一次连接,连接的过程会涉及到多次请求响应交互
    private void processConnection(Socket clientSocket) {
        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、接受从客户端发来的请求,解析请求(将请求转换为字符串)为了方便,直接使用Scanner读取
                //  读取请求并解析. 这里注意隐藏的约定. next 读的时候要读到空白符才会结束.
                //    因此就要求客户端发来的请求必须带有空白符结尾. 比如 \n 或者空格.
                //客户端发来的请求要包含 “\n”
                String request = scanner.next();
                //2、计算请求
                String response = process(request);
                //3、把计算的响应返回给客户端
                //也可以通过下面的这种方式写回,但下面这种方式不好添加 "\n"
                //outputStream.write(response.getBytes(), 0, response.getBytes().length);
                // 也可以给outputStream套上一层,可以更方便的加上 "\n"
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(response);
                //刷新缓冲器
                writer.flush();
                //打印日志
                System.out.printf("[%s : %d] res: %s resp: %s\n", clientSocket.getInetAddress(),
                        clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

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

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

代码解析

        服务器通过accept方法,和客户端建立联系

        如上图,应用程序代码中调用对应的 api 和服务器尝试建立连接,内核就会发起连接的流程。

        服务器的内核就会配合客户端这边的工作来完成连接的建立。

        这个连接建立的过程,就相当于:电话这边在拨号,另一边在响铃;但是要等到用户点击了接听,然后才能进行后续的通信。

        内核建立的连接不上决定性的,还需要用户程序把这个连接进行 "接听" / accept 操作,然后才能进行后续的通信。

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

        有一个客户端连过来,accept 一次就能返回一次。

        有若干个客户端连过来,accept 就需要执行多次。

        第一个socket是负责客户端的连接第二个clientSocket是负责操作服务器内部的业务。

        接下来的方法是处理连接的交互,新创建多出来线程后面讲

        TCP是有连接的和面向字节流的,从下面代码就可以看出来

        TCP的 socket 是可以保存对端的信息InputStream是从网卡读数据,outputStream是从网卡写数据。

        TCP面向字节流,这里的字节流和 文件 中的字节流完全一样,使用文件操作一样的方法和类的对 TCP 的 socket 进行读写。

        如图:

此处的读操作完全可以通过 read 来完成,read 是把收到的数据放到 byte 数组中,后续根据请求处理响应还需要把这个 byte 数组转成 String,比较麻烦,还有一个更简单的方法:Scanner,如上图。

        如图:

        客户端退出之后,服务器就能感知到 “客户端下线” 的操作客户端退出的时候,就会触发TCP的“断开连接”流程,服务器这边的代码就能感知到,对应的Scanner就能够在hasNext这里返回false。

        这里要用 scanner.next() ,因为接受来的请求的字节流,要知道什么时候结束发送过来的请求会带有 "\n",发来的请求中有空白符,比如 \n 或 空格。

        接下来是计算请求,如图:

        因为这里是简单的回显服务器,所以计算就直接返回请求的内容,process方法如图:

        返回响应

        记得要刷新缓冲器。

        

2、客户端代码

public class TchEchoClient {
    Socket socket = null;

    public TchEchoClient(String serverIp, int serverPort) throws IOException {
        //这里的ip和port是直接发给socket的对象
        // 因为TCP是有连接的,所以socket会保存ip和port这些信息
        //因此TcpEchoClient不必保存ip和port
        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) {
                System.out.print("->");
                //1、从控制台输入请求,构造请求
                if(!scannerConsole.hasNext()) {
                    break;
                }
                //发送给服务器的字符串要带有 "\n"
                String request = scannerConsole.next();
                //2、把请求发给发送给服务器,这里需要用println来发生,确保信息里面有 "\n"
                //这里是和服务器的scanner.next()对应的
                writer.println(request);
                //通过flush刷新缓冲器,确保数据真的发出去了
                writer.flush();
                //3、接收服务器返回的响应
                //这里也是和服务器返回的响应逻辑对应,返回的响应带有 "\n"
                String response = scannerNetwork.next();
                //4、把返回的响应显示到控制台
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

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

代码解析

        构造方法里面,如图:

        执行客户端里的构造方法,就会和对应的服务器进行TCP的连接建立流程。(系统内核完成的)。

        这边把内核中连接的流程走完了,服务器这边就能够从 accept 返回

        服务器的建立连接,如图

        processConnection方法内部

        然后进入while循环,执行服务器的内部逻辑,如图:

3、注意事项

        (1)缓冲区

        文件的IO操作都是比较低效的,所以就希望能够让低效的操作,进行的尽量少一些。

        解决方案:引入缓冲区(内存),先把写入网卡的数据放到内存缓冲区中,等攒一波再统一进行发送(把多次IO合并成一次)

        但也有个问题,如果发送的数据很少。此时由于缓冲区还没满,数据就待在缓冲区里,没有被真正发送出去。所以上面的代码中要加入刷新缓冲区的代码。(不然就连数据都发送不出去),如图:不管数据有没有真的发送出去,都要进行刷新缓冲器

        (2)socket的close,释放文件描述符表

        如图:

        clientSocket对象要释放掉因为客户端不止一个,会有很多个,一个客户端发来请求就会占用一个文件描述符表,我们不确定客户端什么时候下线,就可能会一直占用着这些资源(文件描述符表),随着客户端越来越多,又无法释放,文件描述符表就可能占满

        而服务器的sock就不用释放,调用close方法,如图:

        因为这个socket会伴随着服务器的生命周期很长,整个生命周期都会使用到它,而且也只有一个就不用担心占不占满文件描述符表了,像这种情况就不用释放;只要程序退出,socket也会随着进程的销毁一起被释放;UDP的回显服务器的socket不用释放也是因为这个原因。

        因为有finally最后会执行socket的释放,所以,释放了 socket 对象,上述流对象不释放,也问题不大。这两流对象内部不持有文件描述符,只是只有一些内存结构。内存结构可以被 gc 释放。

        但是只释放了流对象,不释放socket,就不行了socket持有了文件描述符表,本质还是要释放文件描述符资源。

        不过这里使用try with resources 的版本,也给它close了,更保险一点

        (3)多线程的应用

        服务器支持多个客户端同时访问是天经地义的,但如果不加多线程方案执 processConnection方法,如图:

        当有多个客户端想同时访问时,第一个客户端先访问服务器,服务器就会从accept这返回(解除阻塞),进入到processConnection中了,接下来就会在scanner.hasNext返回,继续执行服务器逻辑,因为有while循环,完成服务器的逻辑后,把响应返回给客户端执行完上述一轮操作后,循环回来继续再hasNext阻塞,等待下一次循环,知道客户端退出,连接结束,服务器中的循环才会结束、退出,如图:

        当有第二个客户端想访问服务器时,因为第一个客户端还没执行完服务器还在里面的while循环转圈圈呢就无法第二次执行到accept

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

        如果第一个客户端退出了,第二个客户端之前的请求为啥就会被立即出来,而没有丢弃呢?这是因为当前TCP在内核中,每个 socket 都是有缓冲区的客户端发送的数据确实是发了,服务器也收到了,只不过数据是在服务器的接受缓冲区中。

        一旦第一个客户端退出了,回到第一层循环,继续执行第二次 accept ,继续执行 next 就能把之前缓冲区的内容给读出来像是菜鸟驿站,可以存放快递包裹)。

        单个线程,无法既能给客户端提供循环提供服务,又能快速的调用到第二次accept

        所以这里的核心思路就是使用多线程,也是简单的办法,引入多线程,主线程负责执行 accept每次有一个客户端连上来,就分配一个新的线程,由新的线程负责给客户端提供服务。如图:

        而上述没有引用多线程而造成的问题,并不是 TCP 的问题,而是代码本身的问题,因为两层循环嵌套而导致的问题。UDP只有一层循环,所以就不涉及到这种问题,之前的UDP天然的就能处理多个客户端的请求。

        (4)引入线程池的改进

        如图:

        这里每次来一个客户端,就会创建一个新的线程;每次这个客户端结束,就要销毁这个线程。如果客户端比较多,就会使服务器 频繁创建、销毁 线程

        因此,这里我们可以引入线程池,代码如图:

        线程池,解决的事频繁创建销毁的问题。

        如果当前场景是线程频繁创建,但是不销毁呢?(扩展话题)

        每个客户端如果处理过程都很短(网站),线程池可以解决这种频繁创建消耗的问题

        但是每个客户端处理过程都很长呢(例如吃鸡、王者、LOL等待),如果继续使用线程池 / 多线程,此时就会导致当前的服务器上一下积累了大量线程,此时对于服务器的负担就会非常重!!

        为了解决上述积累大量线程的问题,可以引入以下的方案:

                1、协程

                轻量级线程。本质还是一个线程,用户态可以通过手动调度的方式,让这一个线程 “并发” 的做多个任务。(Go / Python)

                2、IO多路复用

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

                基本盘在于,虽然有多个 socket ,但是同一时刻活跃的 socket 只是少数(需要读写数据的 socket),大部分 socket 都是在等,使用一个线程来等多个 socket。        就像去路边摊吃小吃,有很多小吃,我可以在我想吃的小吃店,依次都付款,然后站在这些店的中间,哪个路边摊先做好就去哪个路边摊拿小吃,这样等的过程,就可以节约出很多时间。

4、执行代码

        前述:

        要想执行多个客户端程序,我们要设置一些东西,设置方法如图:

        执行代码后,服务器和客户端交互,如图:

        客户端:

        服务器:

        多个客户端发送数据给服务器:

        可以看到,不同的客户端的进程号不同。而且服务器可以同时被多个客户端请求。

5、客户端和服务器交互的过程

1、服务器启动,阻塞在 accept,等待客户端发来的请求

2、客户端启动

        这里的 new 操作,触发了和服务器之间的建立连接的操作,此时 服务器 就会从 accept 中返回。

 3、服务器从 accept 返回,进入到 processConnection方法中

        执行到 hasNext 这里,产生阻塞,此时虽然连接建立了,但客户端还没发来任何请求,hasNext 阻塞等待请求到达。类似电话通了,但没人说。

4、客户端继续执行到 hasNext,等待用户从控制台写入信息。

5、用户真的输入了,此时 hasNext 就返回了,继续执行这里的发送请求的逻辑。

6、服务器从 hasNext 返回读取到请求内容,并进行处理

        读取到请求,构造成响应,并把响应返回给客户端

        服务器就结束本次循环,开启下一轮循环,继续阻塞在 hasNext 等待下一个请求。

7、客户端读到响应,并且显示出来

        结束本次循环,进入下一次循环,继续阻塞等待在 hasNext 等待用户下一次的输入。


都看到这了,点个赞再走吧,谢谢谢谢谢

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

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

相关文章

谁将主导未来AI市场?Claude3、Gemini、Sora与GPT-4的技术比拼

【最新增加Claude3、Gemini、Sora、GPTs讲解及AI领域中的集中大模型的最新技术】 2023年随着OpenAI开发者大会的召开,最重磅更新当属GPTs,多模态API,未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义,不亚…

基于word2vec 和 fast-pytorch-kmeans 的文本聚类实现,利用GPU加速提高聚类速度

文章目录 简介GPU加速 代码实现kmeans聚类结果kmeans 绘图函数相关资料参考 简介 本文使用text2vec模型,把文本转成向量。使用text2vec提供的训练好的模型权重进行文本编码,不重新训练word2vec模型。 直接用训练好的模型权重,方便又快捷 完整…

软件无线电系列——模拟无线电、数字无线电、软件无线电

本节目录 一、模拟无线电 二、数字无线电 1、窄带数字无线电 2、宽带数字无线电 三、软件无线电本节内容 一、模拟无线电 20世纪80年代的模拟体制(美国的AMPS/欧洲的TACS)被称为第一代移动通信,简称1G,主要目标是为在大范围内有限的用户提供移动电话服务。最主要的…

uniapp遇到的问题

【uniapp】小程序中input输入框的placeholder-class不生效解决办法 解决:写在scope外面 uniapp设置底部导航 引用:https://www.jianshu.com/p/738dd51a0162 【微信小程序】moveable-view / moveable-area的使用 https://blog.csdn.net/qq_36901092/…

Figure与OpenAI 联手推出新机器人;荣耀首款「AI PC」即将发布

▶ Figure 与 OpenAI 联手推出新机器人 AI 机器人公司 Figure 发布了他们与 OpenAI 的合作成果,将 OpenAI 的大模型运用在其机器人 Figure 01 上。 据介绍,OpenAI 大模型加持的 Figure 01 机器人现在可以与人全面对话。 OpenAI 模型为机器人提供了高级…

微信小程序(五十九)使用鉴权组件时原页面js自动加载解决方法(24/3/14)

注释很详细,直接上代码 上一篇 新增内容: 1.使用覆盖函数的方法阻止原页面的自动执行方法 2.使用判断实现只有当未登录时才进行方法覆盖 源码: app.json {"pages": ["pages/index/index","pages/logs/logs"],…

mac删除带锁标识的app

一 、我们这里要删除FortiClient.app 带锁 常规方式删除不掉带锁的 app【如下图】 二、删除命令,依次执行即可。 /bin/ls -dleO /Applications/FortiClient.app sudo /usr/bin/chflags -R noschg /Applications/FortiClient.app /bin/ls -dleO /Applications/Forti…

2024计算机二级3

1. 2. 3. 4. 5. 6. append每次只能添加一个元素,两个元素都在同一个列表内相当于是一个整体 7. d.get后边括号内会存在一个默认值,如果题目给出的选项内没有已经存在的键值名,则会返回后边的默认值 8. 字典是映射数据类型,不属于…

【QT】TCP简易聊天框

我们首先复习一下TCP通信的流程 基于linuxTCP客户端和服务器 QT下的TCP处理流程 服务器先启动(处于监听状态) 各函数的意义和使用 QTcpServer Class *QTcpServer*类提供了一个基于TCP的服务器。这个类可以接受传入的TCP连接。您可以指定端口或让QTcpS…

碳储量监测的新技术:遥感在草原碳汇评估中的潜力

在全球环境问题日益严重的今天,以全球变暖为主要特征的气候变化成为了人类面临的巨大挑战。它威胁着地球的生态平衡,对全球可持续发展构成了严峻的挑战。为了应对这一挑战,各国纷纷采取行动,致力于实现碳达峰和碳中和的目标。 在…

Zabbix 监控 tomcat

zabbix-java-gateway服务组件 zabbix监控tomcat需要用到zabbix-java-gateway组件,它充当zabbix服务和java应用程序之间的网关。它允许zabbix服务器用过java网关与java应用程序进行通信,从而监控和收集java应用程序的性能数据。 zabbix-agent服务&#xf…

gradio部署视频输入输出示例,gradio网页输出视频nan,输出视频无法播放解决方法

gradio部署视频输入输出示例,gradio网页输出视频nan,输出视频无法播放 Opencv不能采用h64格式进行编码解决方案moviepy介绍浏览器接受的视频编码格式:采用h264编码合成视频: gradio部署视频输入输出示例Gradio视频组件使用详解简介…

小程序学习3 goods-card

pages/home/home home.wxml <goods-listwr-class"goods-list-container"goodsList"{{goodsList}}"bind:click"goodListClickHandle"bind:addcart"goodListAddCartHandle"/> <goods-list>是一个自定义组件&#xff0c;它具…

【MIT 6.S081】2020, 实验记录(8),Lab: locks

目录 Task 1&#xff1a;Memory allocator (moderate)</font>Task 2&#xff1a;Buffer cache (hard)</font> Task 1&#xff1a;Memory allocator (moderate) 这个任务就是练习将一把大锁拆分为多个小锁&#xff0c;同时可以更加深入地理解 memory allocator 运行…

PY32离线烧录器功能介绍,可批量烧录,支持PY32系列多款单片机

PY32离线烧录器可以对PY系列单片机进行批量烧录&#xff0c;现支持PY32F002A/002B/002/003/030/071/072/040/403/303芯片各封装和XL2409&#xff0c;XL32F001/003等芯片。PY32离线烧录器需要搭配上位机软件才能使用&#xff0c;上位机软件在我们官网&#xff08;www.xinlinggo.…

JVM基础篇

什么是JVM java虚拟机 JVM的功能 1.解释和运行 对字节码文件中的指令&#xff0c;实时的解释成机器码&#xff0c;让计算机执行 2.内存管理 自动为对象、方法等分配内存 自动的垃圾回收机制&#xff0c;回收不再使用的对象&#xff08;c不会自动回收&#xff0c;相当于降…

QT 如何防止 QTextEdit 自动滚动到最下方

在往QTextEdit里面append字符串时&#xff0c;如果超出其高度&#xff0c;默认会自动滚动到QTextEdit最下方。但是有些场景可能想从文本最开始的地方展示&#xff0c;那么就需要禁止自动滚动。 我们可以在append之后&#xff0c;添加如下代码&#xff1a; //设置编辑框的光标位…

指针的函数传参的详细讲解(超详细)

如果对指针基础知识已经有可以直接跳到 函数的指针传参与解引用&#xff0c;哪里不明白可以评论&#xff0c;随时解答。 目录 所以就有了一句话&#xff1a;指针就是地址&#xff0c;地址就是指针 对于指针在C语言中&#xff0c;指针类型就是数据类型&#xff0c;是给编译器…

PHP极简网盘系统源码 轻量级文件管理与共享系统网站源码

PHP极简网盘系统源码 轻量级文件管理与共享系统网站源码 极简网盘是一个轻量级文件管理与共享系统&#xff0c;支持多用户&#xff0c;可充当网盘程序&#xff0c;程序无需数据库 安装步骤&#xff1a; 1.建议安装在apache环境下&#xff0c;并确保.htaccess可用 2.解压文件…

论文阅读——RingMo

RingMo: A Remote Sensing Foundation Model With Masked Image Modeling 与自然场景相比&#xff0c;RS图像存在以下困难。 1&#xff09;分辨率和方位范围大&#xff1a;受遥感传感器的影响&#xff0c;图像具有多种空间分辨率。此外&#xff0c;与自然图像的实例通常由于重…