欢迎光临
我们一直在努力

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 – 回显程序)

    最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!

    赞(0)
    未经允许不得转载:171主机测评 » TCP回显程序 -- 服务器连接多个客户端
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址