【Java EE初阶十五】网络编程TCP/IP协议(二)

1. 关于TCP

1.1 TCP 的socket api

        tcp的socket api和U大片的socket api差异很大,但是和前面所讲的文件操作很密切的联系

        下面主要讲解两个关键的类:

        1、ServerSocket:给服务器使用的类,使用这个类来绑定端口号

        2、Socket:即会给服务器使用,又会给客户端使用;

        TCP是字节流的,传输的基本单位是Byte;

        所谓连接:通信双方是否会记录保存对端的信息;

       对于UDP来说,每一次发送数据报都要手动在send方法中指定目标的地址(UDP自身没有存储这个信息)

       对于TCP来说,则不需要,前提是需要先把连接建立起来(连接如何建立,不需要我们通过代码进行干预,是系统内核自动负责完成的)

       对于应用程序来说,客户端这边主要是发起“建立连接”动作;

        服务器这边,主要是把建立好的连接从内核中拿到应用程序;

       如果有客户端和服务器建立连接买这个时候服务器的应用程序是不需要做出任何操作(也没有任何感知),内核直接完成了连接建立的流程(三次握手),完成流程之后,就会在内核的队列中排队(这个队列是每一个serverSocket都有这样一个队列),应用程序要想和这个客户端进行通信,就需要通过一个accept方法把内核队列中已经建立好的连接对象,拿到应用程序中;

2 基于TCP实现通信 

2.1 代码实现   

服务器代码:

package network;

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 service = Executors.newCachedThreadPool();
        while (true) {
            // 通过 accept 方法, 把内核中已经建立好的连接拿到应用程序中.
            // 建立连接的细节流程都是内核自动完成的. 应用程序只需要 "捡现成" 的.
            Socket clientSocket = serverSocket.accept();
            // 此处不应该直接调用 processConnection, 会导致服务器不能处理多个客户端.
            // 创建新的线程来调用更合理的做法.
            // 这种做法可行, 不够好
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();

            // 更好一点的办法, 是使用线程池.
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    // 通过这个方法, 来处理当前的连接.
    public void processConnection(Socket clientSocket) {
        // 进入方法, 先打印一个日志, 表示当前有客户端连上了.
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(),
                clientSocket.getPort());
        // 接下来进行数据的交互.
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 使用 try ( ) 方式, 避免后续用完了流对象, 忘记关闭.
            // 由于客户端发来的数据, 可能是 "多条数据", 针对多条数据, 就循环的处理.
            while (true) {
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 连接断开了. 此时循环就应该结束
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 1. 读取请求并解析. 此处就以 next 来作为读取请求的方式.
                // next 的规则是, 读到 "空白符" 就返回.
                String request = scanner.next();
                // 2. 根据请求, 计算响应.
                String response = process(request);
                // 3. 把响应写回到客户端.
                //    可以把 String 转成字节数组, 写入到 OutputStream
                //    也可以使用 PrintWriter 把 OutputStream 包裹一下, 来写入字符串.
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    此处的 println 不是打印到控制台了, 而是写入到 outputStream 对应的流对象中, 也就是写入到 clientSocket 里面.
                //    自然这个数据也就通过网络发送出去了. (发给当前这个连接的另外一端)
                //    此处使用 println 带有 \n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据.
                printWriter.println(response);
                //    此处还要记得有个操作, 刷新缓冲区. 如果没有刷新操作, 可能数据仍然是在内存中, 没有被写入网卡.
                printWriter.flush();
                // 4. 打印一下这次请求交互过程的内容
                System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 在这个地方, 进行 clientSocket 的关闭.
                // processConnection 就是在处理一个连接. 这个方法执行完毕, 这个连接也就处理完了.
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String process(String request) {
        // 此处也是写的回显服务器. 响应和请求是一样的.
        return request;
    }

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

客户端代码:

package network;

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

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 需要在创建 Socket 的同时, 和服务器 "建立连接", 此时就得告诉 Socket 服务器在哪里~~
        // 具体建立连接的细节, 不需要咱们代码手动干预. 是内核自动负责的.
        // 当我们 new 这个对象的时候, 操作系统内核, 就开始进行 三次握手 具体细节, 完成建立连接的过程了.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        // tcp 的客户端行为和 udp 的客户端差不多.
        // 都是:
        // 3. 从服务器读取响应.
        // 4. 把响应显示到界面上.
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            PrintWriter writer = new PrintWriter(outputStream);
            Scanner scannerNetwork = new Scanner(inputStream);
            while (true) {
                // 1. 从控制台读取用户输入的内容
                System.out.print("-> ");
                String request = scanner.next();
                // 2. 把字符串作为请求, 发送给服务器
                //    这里使用 println, 是为了让请求后面带上换行.
                //    也就是和服务器读取请求, scanner.next 呼应
                writer.println(request);
                writer.flush();
                // 3. 读取服务器返回的响应.
                String response = scannerNetwork.next();
                // 4. 在界面上显示内容了.
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

2.2 代码分析 

1、基础内容

        所谓的空白符,是一类特殊的字符(类似于换行,回车符,空格,制表符,翻页付,垂直制表符),后续客户端发起的请求,会议空白符作为结束标记(此处就约定使用\n)

        TCP 是字节流通信方式,每次传输多少个字节,每次读取多少个字节,我们往往会手动约定出,从哪里到哪里是一个完整的数据报.上述这里就是约定了使用 \n 作为数据报的结束标记. 就正好可以搭配scanner.next 来完成请求的读取过程,

        ClientSocket 则是在循环中,每次有一个新的客户端来建立连接,都会创建出新的 clientSocket

        每一次执行到clientSocket语句时,,都会创建新的 clientsockete,并且这个 socket 最多使用到该客户端退出 (断开连接),此时,如果有很多客户端都来建立连接,此时,就意味着每个连接都会创建 clientSocket.当连接断开后clientsocket 就失去作用了,但是如果没有手动 close,此时这个 socket 对象就会占据着文件描述符表的位置

        这里的关闭, 只是关闭了 clientsocket 上自带的流对象,并没有关闭 socket 本身.在这个代码中,需要在方法末尾通过 finally 加上 close,保证当前这里的 socket 能够被正确关闭掉;

2、关于多线程

当前启动两个客户端,同时连接服务器.其中一个客户端(先启动的客户端)一切正常,另一个客户端 (后启动的客户端)则没法和服务器进行任何交豆.(服务器不会提示"建立连接”,也不会针对 请求 做出任何响应,这就是关于多线程的一个很明显的问题;

        第一个客户端过来之后,accept 就返回了,得到一个 clientSocket.进入processConnection
,又进入一个 while 循环,这个循环中, 就需要反复处理客户端发来的请求数据.如果客户端这会没发请求,服务器的代码就会阻塞在scanner.hasNext 这里;

        此时此刻,第二个客户端也过来建立连接了,此时连接是可以建立成功(内核负责的),建立成功之后,连接对象就会在内核的队列里等待代码通过 accept 把连接给取出来,在代码中处理
当前的代码,其实无法第一时间执行到第二次的 accept 

        为了让一个服务器可以同时接待多个客户端,上述问题解决的关键就是引入多线程,让每一个客户端都能进行入到accept方法,进入第二次循环;

3、关于引入线程池

        此时这个服务器,每个客户端都要创建一个线程,如果有很多客户端.频繁的来进行建立连接/断开连接,这个时候就会导致服务器频繁的 创建/销毁 线程,(开销就很大了),所以可以使用线程池,来进一步的优化关于线程开销的问题;

2.3 代码运行分析:

          tcp 程序, 客户端启动,就会和服务器建立连接,服务器这边就能感受到(accept 方法就会返回,进一步的进入到 processConnection 中,如果启动多个客户端,即多个客户端同时和服务器建立连接,默认情况下,IDEA 只允许一个代码只能创建一个进程.通过下图所示操作,勾选了 Allow multiple instances,此时就可以运行多个进程了.

        最后通过使用多线程和线程池的相关内容,完成tcp通信,如下图所示:

ps:本篇文章主要讲解了关于tcp实现通信连接的相关的知识点,如果大家感兴趣的话就请一键三连哦!!!

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

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

相关文章

【高阶数据结构】B+树

文章目录 1. B树的概念2. B树的查找3. B-树 VS B树4. B 树的插入分析 1. B树的概念 B树是B树的变形,是在B树基础上优化的多路平衡搜索树,B树的规则跟B树基本类似,但是又在B树的基础上做了一些改进优化。 一棵m阶的B树需满足下列条件&#x…

Vue+OpenLayers7入门到实战:OpenLayers加载船讯网航海地图瓦片到地图上

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 本章介绍如何使用OpenLayers7在地图上加载船讯网航海地图瓦片到地图上的功能。 适用于海运等海洋相关行业使用。 二、依赖和使用 "ol": "7.5.2"使用npm安装依赖npm install ol@7.5.2使用Yarn安装…

IDEA连接database数据库

文章目录 一、连接数据库1、连接mysql2、连接参数配置3、配置驱动从maven仓库下载:要求联网将提前下载好的jar放到本地目录 4、完成 二、执行sql1、选择要操作的数据库2、执行sql 三、问题1、可能因为时区问题连接不上 一、连接数据库 1、连接mysql 2、连接参数配置…

为什么从没有负值的数据中绘制的小提琴图(Violin Plot)会出现负值部分?

🍉 CSDN 叶庭云:https://yetingyun.blog.csdn.net/ 小提琴图(Violin Plot) 是一种用于展示和比较数据分布的可视化工具。它结合了箱形图(Box Plot)和密度图(Kernel Density Plot)的特…

1、OI 赛事与赛制

赛事简介 信息学奥林匹克竞赛(英语:Olympiad in Informatics,简称:OI)是一门在中学生中广泛开展的学科竞赛,和物理、数学等竞赛性质相同。OI 考察的内容是参赛者运用算法、数据结构和数学知识,通过编写计算机程序解决实际问题的能力。 OI 竞赛种类繁多,仅中国就包括: …

C++基础学习

string char转string vector转string 正则匹配

【LeetCode每日一题】单调栈 581. 最短无序连续子数组

581. 最短无序连续子数组 给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组,并输出它的长度。 示例 1: 输入&am…

地下管线管网三维建模工具MagicPipe3D V3.4.2发布

经纬管网建模系统MagicPipe3D,本地离线参数化构建地下管网三维模型(包括管道、接头、附属设施等),输出标准3DTiles服务、Obj模型等格式,支持Cesium、Unreal、Unity、Osg等引擎加载进行三维可视化、语义查询、专题分析&…

2024年数学建模竞赛汇总——时间轴

美赛已过,好多小伙伴表示已经错过,不清楚什么时候报名,什么时候准备,其实每年数学建模比赛有很多个,各大比赛的级别、报名时间、参赛对象等要求什么呢?小编从竞赛说明、竞赛级别、是否允许跨校、报名费用、…

8.2 新特性 - 透明的读写分离

文章目录 前言1. 安装部署1.1 下载安装包1.2 MySQL Shell1.3 配置 MySQL 实例1.4 启动 ReplicaSet1.5 启动 8.2 Router 2. 测试路由总结 前言 MySQL 8.0 官方推出过一个高可用方案 ReplicaSet 主要由 Router、MySQL Shell、MySQL Server 三个组件组成。 MySQL Shell 负责管理…

Swift Combine 使用从 PassthroughSubject 预定好的发送的事件测试订阅者 从入门到精通二十三

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

第四篇【传奇开心果系列】Python文本和语音相互转换库技术点案例示例:pyttsx3自动化脚本经典案例

传奇开心果短博文系列 系列短博文目录Python文本和语音相互转换库技术点案例示例系列 短博文目录前言一、雏形示例代码二、扩展思路介绍三、批量处理文本示例代码四、自定义语音设置示例代码五、结合其他库和API示例代码六、语音交互系统示例代码七、多语言支持示例代码八、添加…

c#,dotnet, DataMatrix 类型二维码深度识别,OCR,(基于 Halcon)

代码中部分调用的 c 函数参数,具体说明自行研究~(我也是参考的其他资源,还没研究透彻) 例如:HOperatorSet.GenRectangle2() , 2000, 2000, 0, 2000, 2000 这些数字应该是选取的图片解析范围、尺寸&#xff…

如何利用Idea创建一个Servlet项目(新手向)

💕"Echo"💕 作者:Mylvzi 文章主要内容:如何利用Idea创建一个Servlet项目(新手向) Servlet是tomcat的api,利用Servlet进行webapp开发很方便,本文将介绍如何通过Idea创建一个Servlet项目(一共分为七步,这可能是我们写过的…

数据结构-最短路径(Dijkstra算法与Floyd算法)

介绍 对于网图来说,最短路径是指两顶点之间经过的边上权值之和最少的路径,其路径上第一个点记为源点,最后一个为终点。 计算最短路径有两个经典算法,即迪杰斯特拉(Dijkstra)算法与弗洛伊德(Fl…

【医学大模型】Text2MDT :从医学指南中,构建医学决策树

Text2MDT :从医学指南中,构建医学决策树 提出背景Text2MDT 逻辑Text2MDT 实现框架管道化框架端到端框架 效果 提出背景 论文:https://arxiv.org/pdf/2401.02034.pdf 代码:https://github.com/michael-wzhu/text2dt 假设我们有一…

设计模式三:工厂模式

工厂模式包括简单工厂模式、工厂方法模式和抽象工厂模式,其中后两者属于23中设计模式 各种模式中共同用到的实体对象类: //汽车类:宝马X3/X5/X7;发动机类:B48TU、B48//宝马汽车接口 public interface BMWCar {void s…

代码随想录算法训练营第三七天 | 单调递增的数字、监控二叉树

目录 单调递增的数字监控二叉树 LeetCode 738.单调递增的数字 LeetCode 968.监控二叉树 单调递增的数字 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xf…

Linux CentOS stream 9 firewalld

随着互联网行业快速发展&#xff0c;服务器成为用户部署网络业务重要的网络工具&#xff0c;但随之而来的就是更密集的网络攻击&#xff0c;这给网站带来了很大的阻碍。防火墙作为保障网络安全的主要设备&#xff0c;可以很好的抵御网络攻击。 防火墙基本上使用硬件和软件两种…

虚拟机 安装 centos7 带桌面

虚拟机 安装 centos7 流程 https://mirrors.tuna.tsinghua.edu.cn/centos/7.9.2009/isos/x86_64/ CentOS-7-x86_64-DVD-2009.iso vmware 安装 centos7 的时候&#xff0c; 如果 不是 选择的 稍后 安装操作系统 &#xff0c; 会不让你选择配置选项&#xff0c;自动帮你把系统…