【JavaEE网络】TCP套接字编程详解:从概念到实现

目录

    • TCP流套接字编程
      • ServerSocket API
      • Socket API
      • TCP回显客户端服务器


TCP流套接字编程

TCP用的协议比UDP更多,可靠性

提供的api主要有两个类ServerSocket(给服务器使用的socket),Socket(既会给服务器使用也会给客户端使用)

字节流:一个字节一个字节进行传输的
一个tcp数据报,就是一个字节数组byte[]

ServerSocket API

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

ServerSocket 构造方法:

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

ServerSocket 方法:

方法签名方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

Socket API

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

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

Socket 构造方法:

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket 方法:

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

TCP回显客户端服务器

TCP版本的回显服务器,进入循环之后要做的事情不是读取客户端的请求,而是先处理客户端的“连接”

服务器代码流程:

  1. 读取请求并分析
  2. 根据请求计算响应
  3. 把响应写回客户端
//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了=>这个过程类似于“生产者消费者模型”

accept 是把内核中已经建立好的连接,给拿到应用程序中。

但是这里的返回值并非是一个"Connection"这样的对象,而只是一个 Socket 对象,这个 Socket 对象就像一个耳麦一样,就可以说话,也能听到对方的声音

通过 Socket 对象和对方进行网络通信

一次0,主要是经历两个部分

  1. 等(阻塞)
  2. 拷贝数据

此处是先握手吗?

不是!握手是系统内核负责的.写代码过程感知不到握手的过程

此处主要是处理连接,也就是握手之后得到的东西

一个服务器,要对应很多客户端,服务器内核里有很多客户端的连接。虽然内核中连接很多,但是在应用程序中,还是得一个一个的处理的。

内核中的“连接”就像一个一个“待办事项“。这些待办事项在一个 队列 的数据结构中。应用程序就需要一个一个完成这些任务

而要完成任务,就需要先取任务

TCP中涉及到两种socket

  1. serverSocket
  2. clientSocket
Socket clientSocket = serverSocket.accept();
//TCP通信能实现两台计算机之间的数据交互,通信的两端要严格区分为客户端(Client)与服务端(Server)

通过processConnection这个方法(自己实现)来处理一个连接的逻辑

try (InputStream inputStream = clientSocket.getInputStream();//相当于耳麦(clientSocket)的耳机(inputStream)OutputStream outputStream = clientSocket.getOutputStream()) {//相当于耳麦(clientSocket)的麦克风(outputStream)
public class TcpEchoServer {private ServerSocket serverSocket = null;//此处不应该创建一个固定线程数目的线程池,不然就有上限了private ExecutorService service= Executors.newCachedThreadPool();//这个操作就会绑定端口号public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}//启动服务器public void start() throws IOException {System.out.println("服务器启动");while (true) {//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了Socket clientSocket = serverSocket.accept();//把内核中的连接获取到应用程序中了processConnection(clientSocket);}}//通过这个方法来处理一个连接的逻辑private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());//接下来就可以 读取请求,根据请求计算响应,返回响应 三步走了// Socket 对象内部包含了两个字节流对象,可以把这两字节流对象,完成后续的读写工作try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//一次连接中,可能会涉及到多次请求/响应while (true) {//1、读取请求并解析,为了读取方便,直接使用ScannerScanner sc = new Scanner(inputStream);if (!sc.hasNext()) {//读取完毕,客户端下线了System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}//这个代码暗含一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割,(比如换行这种)String request = sc.next();//2、根据请求计算响应String response = process(request);//3、把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据PrintWriter writer = new PrintWriter(outputStream);//  使用PrintWriter的println方法把响应返回给客户端//  此处用println而不是print就是为了在结尾加上\n,方便客户端读取响应,使用Scanner.next读取writer.println(response);//  还需要加入一个"刷新缓冲区"操作//网络程序讲究的就是客户端和服务器能“配合”writer.flush();//“上完厕所冲一下”。//这里加上 flush 更稳妥,不加也不一定就出错!!缓冲区内置了一定的刷新策略//比如缓冲区满了,就会触发刷新; 再比如,程序退出,也会触发刷新......推荐大家把 flush 刷新给加上/*IO操作是比较有开销的,相比于访问内存。进入IO操作次数越多,程序的速度越慢。方法:使用一块内存作为缓冲区,写数据的时候,先写到缓冲区,攒一波数据,统一进入IO。PrintWriter内置了缓冲区,手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在内存缓冲区中*///日志,打印当前的请求详情System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),request, response);}} 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();}
}

以上代码有两个问题:

但没有线程安全问题,因为没有多线程

1、关闭

在这个程序中,设计到两类Socket:

  1. ServerSocket(只有一个,生命周期跟随程序,不关闭也没事)
while (true) {//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了Socket clientSocket = serverSocket.accept();//把内核中的连接获取到应用程序中了processConnection(clientSocket);}
  1. 而Socket在1w个客户端就有1w个Socket,此处的Socket是被反复创建的。因此要确保在连接断开后,socket能被关闭
catch (IOException e) {e.printStackTrace();
} finally {//在finally中加入close,确保当前socket被及时关闭try {clientSocket.close();} catch (IOException e) {e.printStackTrace();}
}
//在trycatch后关闭socket

写到这以上代码中对于Scanner和PrintWriter没有close是否会有文件资源泄露呢?不会

因为流对象持有的资源有两个部分:

  1. 内存(对象销毁,内存就回收了)while循环一圈内存自然销毁
  2. 文件描述符(scanner和printWriter没有持有文件描述符;持有的是inputStream和outputStream的引用,这两个进行关闭了,或者更准确的说是socket对象持有的,socket对象关闭了就ok)

不是每个流对象都持有文件描述符,持有文件描述符是要调用操作系统提供的open方法(系统调用,是要在内核完成的,相当重量/严肃的事情)

2、第二个问题是在写完客户端后再说

客户端代码流程:

  1. 从控制台读取用户的输入
  2. 把输入的内容构造请求并发送给服务器
  3. 从服务器读取响应
  4. 把响应显示到控制台上
public class TcpEchoClient {private Socket socket=null;//要和服务器通信,就需要先知道,服务器所在的位置public TcpEchoClient(String serverIp,int serverPort) throws IOException {//完成这个new操作就完成了tcp连接的建议socket=new Socket(serverIp,serverPort);}public void start(){System.out.println("客户端启动");Scanner scConsole=new Scanner(System.in);try(InputStream inputStream=socket.getInputStream();OutputStream outputStream=socket.getOutputStream()){while(true){//1、从控制台输入一个字符串System.out.print("-> ");String request = scConsole.next();//2、把请求发送给服务器PrintWriter printWriter=new PrintWriter(outputStream);//使用println带上换行,后续服务器读取请求,就可以使用Scanner.next来读取了printWriter.println(request);//别忘记flush,确保数据真的发出去了printWriter.flush();//3、从服务器读取响应Scanner scNetwork=new Scanner(inputStream);String response = scNetwork.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();}
}
//以目前以上的代码执行结果
//服务器
服务器启动
[/127.0.0.1:63945] 客户端上线
[/127.0.0.1:63945] req: 你好, resp: 你好
[/127.0.0.1:63945] req: hello, resp: hello
[/127.0.0.1:63945] 客户端下线
//客户端
客户端启动
-> 你好
你好
-> hello
hello
-> //这里客户端退出了

回到上面遗留的第二个问题

现象:当第一个客户端连接好了之后,第二个客户端不能正确被处理,服务器看不到客户端上线,同时客户端发来的请求也无法被处理,当第一个客户端退出之后,之前第二个客户端发的请求,就能正确响应了。

问题出在:在服务器这边,当一个客户端来了,accept就可以正确返回,进入processConnection,然后进入循环处理该客户端的请求,一直等到这个客户端结束,才能回到start方法中

问题关键在于,处理一个客户端的请求过程中,无法第二次调用accept(即使第二个客户端来了也无法处理)

我们期望能够同时让多个客户端进入调用accept,因此用到多线程

主线程负责找到客户端,在有客户端到来时,创建新的线程,让新的线程负责处理客户端的各种请求

以下是改进方式:

//启动服务器
public void start() throws IOException {System.out.println("服务器启动");while (true) {//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了Socket clientSocket = serverSocket.accept();//把内核中的连接获取到应用程序中了//单个线程不太方便同时完成多个任务,因此要多线程,主线程主要负责寻找客户端,每有一个客户端就创建一个新的线程/*Thread t=new Thread(()->{processConnection(clientSocket);});t.start();*///使用线程池service.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});}
}

TCP程序的时候,涉及到两种写法:

  1. 一个连接中只传输一次请求和响应(短连接)
  2. 一个连接中可以传输多次请求和响应(长连接)

而由于我们只是通过普通的方式创建线程,有一个连接就创建一个线程,如果有多个客户端,频繁连接/断开,服务器就频繁创建/释放线程了,因此我们直接采用线程池的方式

注意:我们上述的不能写成这种方式

try(Socket clientSocket = serverSocket.accept()){service.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});
};
/*
processConnection 和主线程就是不同线程了
执行 processConnection 过程中,主线程 try 就执行完毕了。
这就会导致 clientSocket 还没用完呢,就关闭了
因此,还是要把clientSocket交给processConnection里来关闭
*/

虽然使用了线程池,避免了频繁创建销毁线程,但如果仍然很多客户端,创建大量线程仍有很大开销,可以称为高并发

方法:解决高并发引入了很多技术手段,IO多路复用/IO多路转接(但不是说用了一下子就解决了)

解决高并发(四个字):

  1. 开源:引入更多的硬件资源(本质上是减少线程的数量)
  2. 节流:提高单位硬件资源能够处理的请求数

(同样的请求数,消耗的硬件资源更少)

完整代码

服务器

public class TcpEchoServer {private ServerSocket serverSocket = null;//此处不应该创建一个固定线程数目的线程池,不然就有上限了private ExecutorService service= Executors.newCachedThreadPool();//这个操作就会绑定端口号public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}//启动服务器public void start() throws IOException {System.out.println("服务器启动");while (true) {//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了Socket clientSocket = serverSocket.accept();//把内核中的连接获取到应用程序中了//单个线程不太方便同时完成多个任务,因此要多线程,主线程主要负责寻找客户端,每有一个客户端就创建一个新的线程/*Thread t=new Thread(()->{processConnection(clientSocket);});t.start();*///使用线程池service.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});}}//通过这个方法来处理一个连接的逻辑//服务器一启动,就会执行accept,并阻塞等待,当客户端连接上之后,就会立即执行这个方法private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());//接下来就可以 读取请求,根据请求计算响应,返回响应 三步走了// Socket 对象内部包含了两个字节流对象,可以把这两字节流对象,完成后续的读写工作try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//一次连接中,可能会涉及到多次请求/响应while (true) {//1、读取请求并解析,为了读取方便,直接使用ScannerScanner sc = new Scanner(inputStream);//hasNext这里在客户端没有发请求的时候也会阻塞等待,等到客户端真正发数据或者客户端退出,hasNext就返回了if (!sc.hasNext()) {//读取完毕,客户端下线了System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}//这个代码暗含一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割,(比如换行这种)String request = sc.next();//2、根据请求计算响应String response = process(request);//3、把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据PrintWriter writer = new PrintWriter(outputStream);//  使用PrintWriter的println方法把响应返回给客户端//  此处用println而不是print就是为了在结尾加上\n,方便客户端读取响应,使用Scanner.next读取writer.println(response);//  还需要加入一个"刷新缓冲区"操作//网络程序讲究的就是客户端和服务器能“配合”writer.flush();//“上完厕所冲一下”。//这里加上 flush 更稳妥,不加也不一定就出错!!缓冲区内置了一定的刷新策略//比如缓冲区满了,就会触发刷新; 再比如,程序退出,也会触发刷新......推荐大家把 flush 刷新给加上/*IO操作是比较有开销的,相比于访问内存。进入IO操作次数越多,程序的速度越慢。方法:使用一块内存作为缓冲区,写数据的时候,先写到缓冲区,攒一波数据,统一进入IO。PrintWriter内置了缓冲区,手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在内存缓冲区中*///日志,打印当前的请求详情System.out.printf("[%s:%d] req: %s, req: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),request, response);}} catch (IOException e) {e.printStackTrace();} finally {//在finally中加入close,确保当前socket被及时关闭try {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();}
}

客户端

public class TcpEchoClient {private Socket socket=null;//要和服务器通信,就需要先知道,服务器所在的位置public TcpEchoClient(String serverIp,int serverPort) throws IOException {//完成这个new操作就完成了tcp连接的建议socket=new Socket(serverIp,serverPort);}public void start(){System.out.println("客户端启动");Scanner scConsole=new Scanner(System.in);try(InputStream inputStream=socket.getInputStream();OutputStream outputStream=socket.getOutputStream()){while(true){//1、从控制台输入一个字符串System.out.print("-> ");String request = scConsole.next();//2、把请求发送给服务器PrintWriter printWriter=new PrintWriter(outputStream);//使用println带上换行,后续服务器读取请求,就可以使用Scanner.next来读取了printWriter.println(request);//别忘记flush,确保数据真的发出去了printWriter.flush();//3、从服务器读取响应Scanner scNetwork=new Scanner(inputStream);String response = scNetwork.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();}
}

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

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

相关文章

SQL server 使用教程

1.安装和配置SQL Server: 下载安装包:首先,你需要下载SQL Server的安装包。可以从Microsoft官方网站获取最新版的安装包链接,然后下载适用于你的操作系统的版本。 安装SQL Server:双击运行下载的安装包,按…

CentOS7安装MySQL8.3(最新版)踩坑教程

安装环境说明 项值系统版本CentOS7 (具体是7.9,其他7系列版本均可)位数X86_64,64位操作系统MySQL版本mysql-8.3.0-1.el7.x86_64.rpm-bundle.tar 实际操作 官网下载安装包 具体操作不记录,相关教程很多。 mkdir /o…

锂电池SOH预测 | 基于BP神经网络的锂电池SOH预测(附matlab完整源码)

锂电池SOH预测 锂电池SOH预测完整代码锂电池SOH预测 锂电池的SOH(状态健康度)预测是一项重要的任务,它可以帮助确定电池的健康状况和剩余寿命,从而优化电池的使用和维护策略。 SOH预测可以通过多种方法实现,其中一些常用的方法包括: 容量衰减法:通过监测电池的容量衰减…

QT5制做两个独立窗口

目录 增加第二个窗口 主窗口文件添加一个私有成员为子窗口 定义两个槽函数和 关联按钮和子窗口和主窗口 添加子窗口成员 子窗口处理函数 补充回顾 增加第二个窗口 1、 2、 3 主窗口文件添加一个私有成员为子窗口 在mainwidget.h文件 同时添加两个槽;来处理…

Linux 系统上安装 NVIDIA 驱动程序失败(X server问题)

报错信息: ERROR: You appear to be running an X server; please exit X before installing. For further details, please see the section INSTALLING THE NVIDIA DRIVER in the README available on the Linux driver download page at www.nvidia.com. ERROR: …

Docker: 如何不新建容器 修改运行容器的端口

目录 一、修改容器的映射端口 二、解决方案 三、方案 一、修改容器的映射端口 项目需求修改容器的映射端口 二、解决方案 停止需要修改的容器 修改hostconfig.json文件 重启docker 服务 启动修改容器 三、方案 目前正在运行的容器 宿主机的3000 端口 映射 容器…

vue2实现面包屑功能

目录 1. store/index.js 2. router/index.js 3. Header.vue 在Vue 2中实现面包屑导航是一种常见的前端实践,它可以帮助用户了解当前页面在网站结构中的位置,并快速导航到上一级或根目录。以下是使用Vue 2实现面包屑导航的基本步骤: 1. store/index.js state中定义一个面…

python 关键字(await)

2、await 在Python的异步编程中,await关键字扮演着至关重要的角色。对于初学者来说,理解await的使用和背后的概念可能有些困难,但对于有经验的开发者来说,掌握它则是编写高效、响应性强的代码的关键。下面我将从基础到高级,逐步解析await关键字。 基础知识:await是什么?…

NLP(11)--词向量

前言 仅记录学习过程,有问题欢迎讨论 one-hot 编码 i love u [1,2,3] 词向量训练目标: 如果两个词在文本出现,它的前后出现的词相似,则这两个词语义相似 cbow(基于窗口预测词)缺点 :输出层是vocab_size 会很大 收敛速度会很慢…

【综述】多核处理器芯片

文章目录 前言 Infineon处理器 AURIX™系列 TC399XX-256F300S 典型应用 开发工具 参考资料 前言 见《【综述】DSP处理器芯片》 Infineon处理器 AURIX™系列,基于TriCore内核,用于汽车和工业领域。 XMC™系列,基于ARM Cortex-M内核&…

test4282

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。关…

2024五一杯数学建模A题思路分析-钢板最优切割路径问题

文章目录 1 赛题选题分析 2 解题思路3 最新思路更新 1 赛题 A题 钢板最优切割路径问题 提高钢板下料切割过程中的工作效率,是模具加工企业降低成本和增加经济效益的重要途径,其中钢板切割的路径规划是钢板切割过程的一个关键环节。 钢板切割就是使用特殊…

2024 五一杯高校数学建模邀请赛(C题)| 煤矿深部开采冲击地压危险预测 |建模秘籍文章代码思路大全

铛铛!小秘籍来咯! 小秘籍团队独辟蹊径,构建了这一题的详细解答哦! 为大家量身打造创新解决方案。小秘籍团队,始终引领着建模问题求解的风潮。 抓紧小秘籍,我们出发吧~ 让我们看看五一杯的C题! 完…

【Jenkins】持续集成与交付 (十二):Jenkins构建(Maven类型项目)和部署项目

🟣【Jenkins】持续集成与交付 (十二):Jenkins构建(Maven类型项目)和部署项目 一、安装Maven Integration插件二、创建Maven项目三、配置项目3.1 配置执行构建操作3.2 执行Maven项目构建3.3 部署项目3.5 再次构建3.4 验证访问四、总结💖The Begin💖点点关注,收藏不迷…

ChatGPT 网络安全秘籍(一)

原文:zh.annas-archive.org/md5/6b2705e0d6d24d8c113752f67b42d7d8 译者:飞龙 协议:CC BY-NC-SA 4.0 前言 在不断发展的网络安全领域中,由 OpenAI 推出的 ChatGPT 所代表的生成式人工智能和大型语言模型(LLMs&#xf…

MATLAB初学者入门(30)—— 数据库开发

在MATLAB中实现数据库开发涉及连接数据库、执行SQL查询、处理查询结果以及将数据用于分析和可视化。MATLAB提供了数据库工具箱,该工具箱支持与多种数据库系统的连接,包括但不限于MySQL、Oracle、SQL Server、PostgreSQL等。 案例分析:使用MA…

【软件开发规范篇】JAVA后端开发编码格式规范

作者介绍:本人笔名姑苏老陈,从事JAVA开发工作十多年了,带过大学刚毕业的实习生,也带过技术团队。最近有个朋友的表弟,马上要大学毕业了,想从事JAVA开发工作,但不知道从何处入手。于是&#xff0…

软件网关--Nginx

Nginx 的多功能用途 Nginx 是构建现代化分布式应用程序的重要工具,它的功能远不止于高性能的 Web 服务器和反向代理。它还能充当 API 网关,处理前后端通信、认证、授权、监控等任务。以下是 Nginx 的主要用途: 前后端通信代理:N…

公共 IP 地址与私有 IP 地址区别有哪些?

​  IP 地址是分配给互联网上每个设备的唯一数字 ID。 IP 地址可以在 Internet 上公开使用,也可以在局域网 (LAN)上私有使用。本文,我们主要探讨公共 IP 地址和私有 IP 地址之间的区别。 公共IP地址:公共IP地址是用于访问Internet的向外的I…

头歌:SparkSQL简单使用

第1关:SparkSQL初识 任务描述 本关任务:编写一个sparksql基础程序。 相关知识 为了完成本关任务,你需要掌握:1. 什么是SparkSQL 2. 什么是SparkSession。 什么是SparkSQL Spark SQL是用来操作结构化和半结构化数据的接口。…