【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() {@Overridepublic 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.hqwc.cn/news/466660.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

webpack面试解析

参考: 上一篇webpack相关的系列:webpack深入学习,搭建和优化react项目 爪哇教育字节面试官解析webpack-路白 1、Webpack中的module是什么? 通常来讲,一个 module 模块就是指一个文件中导出的内容,webpack…

Linux线程 分离和同步与互斥 条件变量

Linux线程 分离和同步与互斥 条件变量 1. 分离线程2. 线程互斥与互斥量3. 线程同步与竞态条件4. pthread库与条件变量5. 生产者-消费者 1. 分离线程 什么是线程分离? 线程分离是指线程在结束时,操作系统会自动回收其资源,而无需其他线程显式地…

EasyCaptcha,开源图形验证码新标杆!

引言: 随着互联网的普及,验证码已成为网站和应用程序中不可或缺的安全组件。它能够有效地防止自动化攻击、垃圾邮件和机器人活动。在众多验证码解决方案中,Easy-captcha以其简单易用和高度可定制的特点受到了开发者的青睐。本文将指导读者如…

leetcode:买卖股票最佳时机二

思路: 使用贪心算法:局部最优是将买卖过程中产生的正数进行相加,进而使得最后结果最大(全局最优)。 price [7,1,5,10,3,6,4] -6,4,5,-7,3,-2 正数相加就得到了最大 代码实现: 1.循环中下标从1开始 …

AI大模型开发架构设计(10)——AI大模型架构体系与典型应用场景

文章目录 AI大模型架构体系与典型应用场景1 AI大模型架构体系你了解多少?GPT 助手训练流程GPT 助手训练数据预处理2个训练案例分析 2 AI 大模型的典型应用场景以及应用架构剖析AI 大模型的典型应用场景AI 大模型应用架构 AI大模型架构体系与典型应用场景 1 AI大模型架构体系你…

蓝牙BLE学习-蓝牙广播

1.概念 什么叫做广播,顾名思义就像广场上的大喇叭一样,不停的向外传输着信号。不同的是,大喇叭传输的是音频信号,而蓝牙传输的是射频信号。 BLE使用的是无线电波传递信息,就是将数据编码,调制到射频信号中发…

【JavaEE】_CSS选择器

目录 1. 基本语法格式 2. 引入方式 2.1 内部样式 2.2 内联样式 2.3 外部样式 3. 基础选择器 3.1 标签选择器 3.2 类选择器 3.3 ID选择器 4. 复合选择器 4.1 后代选择器 4.2 子选择器 4.3 并集选择器 4.4 伪类选择器 1. 基本语法格式 选择器若干属性声明 2. 引入…

春节过半,预定的计划还没有开始

春节前就立下雄心勃勃的计划,想利春节假期开始搭一个人脸通WEB管理软件。但眼看春节过半,自己还没有开始动手呦。哎,突然紧张起来了。初二初三身体都不太舒服,不知道是怎么回事就感冒了,今晚更是高烧39.5,感…

python-分享篇-GUI界面开发-PyQt5-在窗口中弹出等待提示框

代码 # *_* coding : UTF-8 *_* # 文件名称 :waiting_prompt.py # 开发工具 :PyCharmfrom window import Ui_MainWindow # 导入窗体ui类 from PyQt5.QtWidgets import QMainWindow, QApplication # 导入qt窗体类 from PyQ…

【51单片机】LED点阵屏(江科大)

9.1LED点阵屏 1.LED点阵屏介绍 LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。 2.LED点阵屏工作原理 LED点阵屏的结构类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已。原理图如下 每一行的阳极连在一起,每一列…

C++初阶:适合新手的手撕vector(模拟实现vector)

上次讲了常用的接口:C初阶:容器(Containers)vector常用接口详解 今天就来进行模拟实现啦 文章目录 1.基本结构与文件规划2.空参构造函数(constructor)4.基本函数(size(),capacity(),resize(),reserve())4.增…

AcWing 802. 区间和 离散化

文章目录 题目链接题目描述解题思路代码实现总结 题目链接 链接: AcWing 802. 区间和 题目描述 解题思路 离散化是一种常用的技巧,它能够将原始的连续数值转换为一组离散的值,从而简化问题的处理。在这段代码中,离散化的过程主要分为三个步…