TCP回显程序 -- 服务器连接多个客户端
TCP回显程序 – 服务器连接多个客户端
文章目录
- TCP回显程序 -- 服务器连接多个客户端
- 观前提醒
- 1. IDEA中,同一个类,启动多个
- 2. UDP的客户端,启动多个,连接服务器端
- 3. TCP的客户端,启动多个,无法连接到服务器端
- 4. 原因分析
- 5. 解决问题(多线程)
- 6. 减少线程开销(线程池)
- 7. 线程简聊
- 8. 简聊 IO多路复用
- 举例说明
- 归纳
- 总结
观前提醒
如果你是第一次点击这篇博客的,你需要先看这篇博客:Java网络编程(4):(socket API编程:TCP协议的 socket API – 回显程序)
这篇博客,是从上面这篇博客分离出来的,旨在减少单篇博客的字数。
1. IDEA中,同一个类,启动多个
连接多个客户端,换句话说,也就是要多个客户端程度,连接同一个服务器。
启动多个客户端,也就是同一个类,要启动多个。
在IDEA中,默认同一个类,只能启动一个。
我们需要将表示客户端的类(TCPEchoClient类),设置为:可以同时启动多个实例。
这样我们才可以同时启动多个相同的类。
英文界面修改:

中文界面修改:


2. UDP的客户端,启动多个,连接服务器端
日常生活中,一个 App 的服务器,正常来说,是只有一个的。
但是,客户端,是很多个用户的手机,都在使用的。
避免不了,同一个时间点内,有多个客户端在同时访问服务器,那么,服务器就得同时给这几个客户端,提供服务。
也就是说,服务器的正常情况是:
一个服务器,能同时给多个客户端提供服务。
这是服务器的一个基本特点。
在 UDP 的服务器中,是可以这么干的:

但是,在使用 TCP 编写的这个程序里面,可以这样子做吗?
3. TCP的客户端,启动多个,无法连接到服务器端
我们先启动TCP的服务器端程序,再启动多个客户端程序。

我们可以看到,当我们启动一个 客户端1 之后,发送了一个 “ 你好 ”,服务器计算响应之后,同样返回了一个 “ 你好 ” 给客户端1。
但是,当我们再一次启动同一个客户端的时候,此时,客户端2 同样发送了一个 “ 你好 ” ,但是,服务器并没有处理这个客户端发送的请求数据,没有返回响应给 客户端2。
此时,我把 客户端1 的程序,给他结束掉。

可以看到,当我把 客户端1 给关闭之后,客户端2 的请求,才能被接收并处理,最后将响应返回。
那么,上述问题,是为什么呢?
4. 原因分析
上述情况,是代码的问题,我们目前所写的代码,导致服务器程序,同一个时间内,只能处理 一个 客户端的请求。
从代码开始启动之后,我们来分析一下:

原因就出现在这句代码上:!scanner.hasNext()
如果 客户端1 不发送请求过来,服务器,就会阻塞在 hasNext 这句代码。
我们这个程序,目前的状况是:
无法同时等待用户请求 和 进行客户端的连接(accept)。
看代码,我们有两个 while 循环:
第一个 while 循环:accept() 方法,与客户端建立连接
第二个 while 循环:等待用户发送请求,并处理请求
也就是,我们只有处理完用户发送过来的请求,才能跳出第二个循环。
回到第一个 while循环,继续与新的客户端,建立连接
等待用户发送请求的时候,程序在阻塞等待当中,没法进行客户端的连接(accept)。
这个时候,有新的客户端想要连接服务器的时候,就没法连接了。
那么我们有没有什么方法,能解决这个阻塞等待的问题?
能让这两个步骤,分开执行,兵分两路?
答:多线程!!!
5. 解决问题(多线程)
多线程,它的诞生,就是为了解决这样的问题,为这种场景服务的。
如何使用多线程解决问题呢?
我们明确这么一个方向:
在我们的主线程里面,我们就专注于 和客户端建立连接。
每次和 新的客户端建立连接,我们就创建一个新的线程,由这个新的线程来处理客户端的请求。
这就很像我们在编写服务器代码中提到的那样:
销售小哥,负责揽客
销售小姐姐,负责讲解楼盘
代码:
// 服务器需要不断的读取客户端的请求数据,用一个死循环实现这一点
while(true){
// 对于 TCP 来说,需要先处理客户端发来的连接
// clientSocket 这个变量,就表示和服务器和客户端进行交流的一个对象,
// 后续,就通过读写 clientSocket ,和客户端进行通信
// 如果没有客户端发起连接,此时 accept 就会阻塞等待
// 主线程:负责进行客户端的连接(accept)
// 每次和 新的客户端建立连接,我们就创建一个新的线程,由这个新的线程来处理客户端的请求
Socket clientSocket = serverSocket.accept();
// 创建线程
Thread thread = new Thread(() ->{
// 处理一个客户端的请求和响应
processConnect(clientSocket);
});
// 启动线程
thread.start();
}
- 首先,通过主线程,我们和客户端建立连接,
- 随后,创建一个线程,处理这次客户端的请求,将这个任务,交给这个线程之后,这个线程的创建和启动立刻完成。
之后,这个线程,处理完了一个客户端的所有请求之后,就会销毁。
- 最后,主线程继续等待与下一个客户端建立连接。
以上过程,循环往后。
此时,我们再来看看修改之后的效果:

此时,我们就可以看到,服务器能同时对 客户端1 和 客户端2 的请求,都做出响应。
完成了 服务器,能在同一个时间内,对多个服务器提供服务 的效果。
简单总结就是:
主线程,是 accept 在工作,不断的与新的客户端建立连接。
accept 与新的客户端建立连接之后,创建新的线程,客户端发送的请求在这个新的线程中,进行处理。
主线程中,与多少个客户端建立连接,就会创建多少个线程。
6. 减少线程开销(线程池)
目前这个代码,有客户端建立连接,就创建线程,客户端断开,线程就销毁了。
这种方式,也是有一个小小的弊端:哪怕线程比较轻量化,但也架不住量大。
如果有一瞬间,有大量的客户端建立连接,会创建大量的线程,开销很大。
是否有办法优化一下这种情况?
答:有,使用线程池。
我们这里,选择使用 newCachedThreadPool,而不是 newFixedThreadPool。
原因:
使用 newFixedThreadPool ,表示同时处理的客户端的连接数目就固定了,不够灵活。
因为你有多少个客户端,是不好去预测的,无法判断准确的数量。
使用 newCachedThreadPool,能够无限扩容,能同时处理多个客户端的连接数目,足够灵活。
代码:
// 使用 newCachedThreadPool,能够无限扩容,能同时处理多个客户端的连接数目,足够灵活。
ExecutorService executorService = Executors.newCachedThreadPool();

// 使用线程池的方式进行调整
executorService.submit(()->{
// 处理一个客户端的请求和响应
processConnect(clientSocket);
});

当某一时刻,有大量的客户端建立了连接,线程池中,是提前创建好了部分线程的,这些线程就可以立即投入工作,避免了线程同一时间,开销过大的问题。
7. 线程简聊
无论是多线程,还是线程池,都意味着,一个客户端对应一个线程。
一个主机,能够创建的线程,是有上限的!!!
线程,不是创建的越多越好。
线程创建的越多,CPU的利用率无法提高,系统的调度速度也会降低。
同时,创建线程,是会消耗系统资源的,而系统资源,是有限的,所以,能创建的线程数目,也是有限的。
一般来说,一个主机,创建几千个线程,已经不容易了。
再想创建更多线程,是不靠谱的。
2000年的时候,有这么一个问题:10K 问题,处理 10000 个客户端,怎么进行处理?
后面进一步发展,又出现一个问题:10M(M表示百万) 问题,处理 1000万 个客户端,怎么进行处理?
以上两个问题,使用一种方式进行解决:IO多路复用,也叫做IO多路转接。
感兴趣的,可以自己去了解了解,这里就不介绍了。
这里的技术,除了 IO多路复用,还有 Java NIO,Netty知名网络框架。
我们当前学的 IO,也叫做 BIO(也叫:阻塞IO,即 “ blocking IO ”)
NIO,叫做 非阻塞IO,即 “NON blocking IO”
8. 简聊 IO多路复用
上面说到,如果客户端连接的数量进一步增加,此时多线程/线程池,就会产生大量的线程,但是,一个主机上,能创建的线程,是有限的。
此时,就需要采用 操作系统中,内置的 IO多路复用 的机制。
IO多路复用 的本质就是:一个线程同时处理多个客户端的请求。
为什么可以这样做呢?
其实,当你多个客户端都连接上一个服务器的时候,此时,并不是所有的客户端,都会同时发送请求的。
有一部分的客户端发送请求,其他绝大多数都在等待请求当中。
此时,一个线程,就可以先处理那些已经发送请求的客户端,其他客户端,可以先等待当中。
举例说明
我们举一个平时生活中一个很常见的例子:吃饭
你是否会想过很多次这样的问题:晚上吃什么?
假设,我现在有了一个家庭,我晚上,想吃蛋炒饭,我老婆,想吃油泼面,我的小孩,想吃煎饼果子。
此时,我是下去楼下的小吃街去买这些食品的,那么,购买这些食品的方案,可以有三种:
方案一:我一个人独自去买,等待三次
我去小吃街,先买蛋炒饭,等待老板炒完蛋炒饭给我,再去买油泼面,等待老板做好面之后,我最后去买煎饼果子。
这样的方式,就是典型的 单线程 的方式,先处理完一个,再处理一个。
这种方式,效率很低!!!
方案二:三个人一起出动,各自买各自的食物
我去买蛋炒饭,我老婆去买油泼面,小孩子自己去买煎饼果子。
这种方式,就是多个线程的方式,一个线程,处理一个任务。
这种方式,效率很高,但是,开销很大!!!
方案三:还是我一个人去买
我此时来到蛋炒饭这边,和老板说:帮我炒份蛋炒饭,我等下过来拿,
接下来到油泼面这边,和老板说:来份油泼面,我等下过来拿,
最后来到煎饼果子这边,和老板说:来份煎饼果子,等下过来拿。
此时,我就在等,同时等这三份食物做好。
等到哪一份食物做好了,老板就叫我过去拿。
最后,我只用了一份等待的时间,同时等待三个任务的完成。
只用了一个线程,减少了开销,同时效率也高。
那么,方案三这种方式,就是 IO多路复用 的工作方式。
归纳
多个客户端,可以理解为多个小摊的老板。
每个客户端,绝大部分时间都是沉默的,工作线程只需要等待就可以了。
等到客户端发来数据的时候,线程再来处理就可以了。
并且,多个客户端同时发数据的概率,比较小。
综上,虽然我们只有一个线程,但是这种工作方式,也能处理多个客户端了。
那客户端很多呢?比如 3000个,30000个 的时候呢,冲突的概率不就大了吗?
那我们又不是只有一个线程,可以多搞几个线程,就可以处理更多了客户端了。
总结来说:
IO多路复用,目的就是可以让 一个线程,能够处理多个客户端了。
减少了系统资源的开销。
IO多路复用,是当前开发服务器的主流的核心技术。
IO多路复用,是操作系统内置的,只需要调用 API 即可。
Java这边,JDK则是把 IO多路复用 ,封装成了 NIO。
总结
看完这篇博客,就可以回到这篇博客,继续阅读了。
Java网络编程(4):(socket API编程:TCP协议的 socket API – 回显程序)
最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!







