TCP编写服务器,客户端以及遇到的两个问题,Socket,ServerScket 类,flush(),方法。以及多线程解决,及改进的线程池写法,IO多路复用的思想,C10K,C10M的阐述。万字超细

简介: TCP编写服务器,客户端以及遇到的两个问题,Socket,ServerScket 类,flush(),方法。以及多线程解决,及改进的线程池写法,IO多路复用的思想,C10K,C10M的阐述。万字超细

一、💛

TCP分量比UDP更重,协议更多,字节流,一个字节一个字节传输,一个TCP数据报就是一个字节数组,byte[](也就是说不用整我们那个文件报)

主要分为两个类:

ServerSocket:给服务器使用的Socket。

Socket:既会给服务器用,也会给客户端使用

“连接”连接其实更准确的是说,服务器与客户端建立绑定关系,互相保存对方信息。

握手是系统的内核负责->一个服务器,要对应很多客户端,服务器内核里面有很多客户端连接->虽然内核中的连接很多,但是在应用程序中,还是要一个一个处理的~~

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

要完成任务,也就需要先取任务:

serverSocket.accept()。->用户执行accept的时候,此时客户端还没来呢,所以会进行阻塞,直到有客户端连接成功为止。

二、 💙

把内核中的连接获取到应用程序,过程类似于“生产者,消费者模型”

客户端和服务器建立连接的时候,服务器就会和用户(客户端)进行一系列的数据交互,成为“握手”,这个过程建立完成了之后,连接就建立完成了,此时一个连接就会产生一个元素,消费的时候取出元素,放到应用程序里面处理,这也就构成了生产者,消费者模型

三、💜

一次IO操作,主要分为两个部分:

1.等(阻塞)

2.拷贝数据

介绍部分服务器核心代码

//accept是把内核中已经建立好的连接,给拿到应用程序中,
//但是这里的返回值并非是一个Connection这样的对象,
//而只是一个Socket对象,后续是Socket进行操作,换句话说:你要买房
//这个accept()相当于路边的销售小哥,给你拉进屋子不管你了,
//那个金牌销售小姐姐(Socket)给你服务
Socket clientSocket=serverSocket.accept();

1.Socket对象,相当于一个耳麦,可以对他说话,也可以听到声音,通过Socket对象和对方通信~,不用管对方到底是什么人,冲这个耳麦说话就行。

//inputStream相当于一个耳机,用来接收信息
//outputStream相当于一个麦克风,用来输出信息
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){····}

2.String request=scanner.next();

next是读取数据,一直读到空白符号才结束

空白符包括但不限于:换行(\n让光标另起一行),回车(\r让光标回到行首),空格,制表符,翻页符,垂直制表符

3.flush()

PrintWrite writer=new PrintWriter(outputStream);
//flush()刷新的意思,就相当于冲马桶,一下子全下去了
writer.flush()

IO 操作是比较有开销的,可用于访问内存,进行IO次数越多,程序速度越慢,使用一块内存作为缓存区,写数据的时候,先到缓冲区里,攒一波数据,统一进行IO,PrintWriter内置了缓冲区,所以手动刷新,确保这里的数据真正的全部通过网卡发出去了,而不是残留在内存缓冲中的(瞬间有味道了哈哈哈💨💨💨)

加flush()属于是确保稳定性,不加也不一定出错,缓存区内置了一定的刷新策略~比如缓冲区满了,就会触发刷新,再比如程序退出,也会触发刷新,推荐大家加上flush()

4.

🐷全缓冲:(往网课/文件里面写),一般是全缓冲,不会收到ln的影响

🐷行缓冲:(特殊情况,换行会刷新)一般在标准输入输出这里~(往控制台打印),控制台中,用户输入一条指令,都是用enter按钮进行确认的,enter本质是\n(Linux中),\r\n(windows)

5.

这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行)

               String request = scanner.next();是什么意思呢,代码演示一下,也就是这样,我们正常输入I love you 连一起的输入,他会当成是我们先发一个I,然后进行回车,再发送love,再回车,返回你love,你再发送you,返回你you

四、❤️

具体实现服务器,底下的只是大部分的实现逻辑,现在,会不会有小可爱们看出哪里有问题呢

import sun.nio.ch.sctp.SctpNet;
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;
public class TcpEchoServer {
//建立一个ServerSocket对象,构造方法去设置他的端口号
    private ServerSocket serverSocket=null;
    public TcpEchoServer(int port) throws IOException{
        serverSocket=new ServerSocket(port);
    }
    public void  start() throws IOException {
        System.out.println("服务器启动");
        while (true){
//会停在这里,知道客户端上线,
            Socket clientSocket=serverSocket.accept();
//具体的操作方法写到这个方法之中
            processConnection(clientSocket);
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
//getInetAddress()是获取当前与Socket对象关联的InetAddress对象的字符串表示,在Socket类中getInetAddress()方法返回与(Socket)连接的远程对象,调用完这个方法之后,可以进一步调用toString()方法来获取改InetAddress对象的字符串表示。
//getPort()是返回与Socket连接的远程端口号。
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来就可以读取请求,根据请求计算响应,返回响应,然后进行老三步
        //Socket对象内部包含了两个字节流对象,可以把这俩字节流获取到,完成后,续写读写操作。
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            //一次连接中,可能涉及到多次请求/响应
            while (true) {
                //1.读取请求并且解析,为了读取方便,使用Scanner
                Scanner scanner = new Scanner(inputStream);
//在客户端,没有发送请求的时候,也会进行阻塞,一直会阻塞到客户端真正发送了数据,或者客户端退出,hasNext()就返回了。
                if (!scanner.hasNext()) {
                    //1.读取完毕,客户端下线
                    System.out.printf("[%s:%d]客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                //这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行)
                String request = scanner.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();
                //日志,打印当前的请求详情
//getInetAddress()是获取当前与Socket对象关联的InetAddress对象的字符串表示,在Socket类中getInetAddress()方法返回与(Socket)连接的远程对象,调用完这个方法之后,可以进一步调用toString()方法来获取改InetAddress对象的字符串表示。request表示你的请求,response表示返回服务器的请求 
                System.out.printf("[%s:%d] req:%s,resp:%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
            }
        }
    }
    public String process(String request){
        return  request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

好了我要公布答案了哦——

没错就是我们的关闭操作,因为Socket对象持有的文件描述符和之前那个一样,是需要关闭的,如果不关闭,就会这个文件描述符一直保持打开,关闭Socket对象的主要原因——避免资源泄露和过度占用。(带走你的年终奖大法)下面是改正

import sun.nio.ch.sctp.SctpNet;
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;
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("服务器启动");
        while (true){
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来就可以读取请求,根据请求计算响应,返回响应,然后进行老三步
        //Socket对象内部包含了两个字节流对象,可以把这俩字节流获取到,完成后,续写读写操作。
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            //一次连接中,可能涉及到多次请求/响应
            while (true) {
                //1.读取请求并且解析,为了读取方便,使用Scanner
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    //1.读取完毕,客户端下线
                    System.out.printf("[%s:%d]客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                //这个代码暗含一个约定,客户端发来的请求,得到的文本数据,同时还得是带有空白符号进行分隔(比如换行)
                String request = scanner.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();
                //日志,打印当前的请求详情
                System.out.printf("[%s:%d] req:%s,resp:%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
            }
            }
        catch (IOException e){
            e.printStackTrace();
        }finally {
            //在finally中加上close操作,确保当前socket被及时关闭
            clientSocket.close();
        }
    }
    public String process(String request){
        return  request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

五、💚

下面来几个问题理解一手

当服务器接收到客户端的请求并对其进行处理后,可以通过获取一个PrintWriter对象,并使用它的print或者println方法将响应文本写入输出流。该输出流最终会传输到客户端,客户端可以读取并处理这些响应程序

🐣🐣客户端和服务器的inputStream和OutputStream是四个文件在接收传输数据吗?

答:准确的说是四个流对象,实际上是两个流对象。,通常情况下,是一个流对应一个文件的,这里是一个socket对象(文件)对应两个流对象。

写到try()里面是能自动关闭的,但是try{}这个里面就需要手动关闭,try with resources语法。

🐔🐔Scanner和PrintWriter是否进行close()呢,会不会有文件资源泄露呢?——不会!

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

1.内存(对象销毁,内存就回收了)

while循环一圈,内存自然销毁,scanner和printWriter没有文件描述符,持有的是inputStream,OutputStream引用(我们用try()已经关闭了)更准确的说持有文件描述符的是Socket对象。

2.文件描述符

六、 💖

具体去实现客户端代码

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;
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() throws IOException {
        System.out.println("客户端启动");
        Scanner scannerConsole = new Scanner(System.in);
//Socket对象内部包含两个内部字节流对象,可以把这两个字节流获取到,完成后续的读写操作。
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                //1.从控制台输入字符串
                System.out.println("->");
//所有的next()的意思,都是一个空格,一个空格或者回车的这么读,比如你输入
//I love you ,他就会先输出I,在love 再you,他不会你再次输入,而是读取整个后存起来,一个一个一个读
                String request = scannerConsole.next();
                //2.把请求发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                //使用println带上换行,后续服务器读取请求,就可以用scanner.next来获取
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应,也就是读取返回值
                Scanner scannerNetWork = new Scanner(inputStream);
//注意这里的next()的意思是,一块一块打印,而不是一整块打印,所以也需要while
                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();
    }
}

但是他这样也有问题,当我们选择再开一个客户端的时候,发生了问题,第一个客户端好用,但是第二个客户端陷入阻塞,此时我关闭第一个客户端,第二个客户端变成第一个客户端,他又好使了。

这就说明我们的代码出现了bug,发现是我们的代码结构出现了问题。

//服务器的代码中你接收到一个客户端的请求,那么此时线程进行这个方法中,
//那么你这个线程方法还没有结束,我们的第二个客户端该怎么启动呢,
//你买房,那个销售小哥,给你拉过来,再给你讲房子如何如何,那么第二个客人该怎么拉过来呢?
 public void  start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

此时破局之道在哪呢——看我那个比喻是不是感觉人像是不够用,感觉来个销售的小姐姐,那不是美滋滋 💖 💖 💖,这就需要用到我们之前的多线程了,这样两个客户端的问题就解决了。

//只需要改动服务器的这里就行,但是你也看到了这里
//我们进行频繁的创建线程。会造成一个资源的占用。
 public void  start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket=serverSocket.accept();
            Thread t=new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }

所以改进方法:使用我们的线程池

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){
            Socket clientSocket=serverSocket.accept();
            //使用线程池创建问题
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

这时候我们来想一下第一个问题:关闭的问题,我在使用的时候用 try with resources OK不?

//此时这么写这个代码有错误,processConnection和主线程是不同的线程了
//此时你这么写,在执行processCOnnection过程中,主线程try 就执行完毕了.
//这就会导致,clientSocket还没用完,我就先关闭了
//因此要把clientSocket交给ProcessConnection关闭
 try(Socket clientSocket=serverSocket.accept()){
            //使用线程池创建问题
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
}

虽然这里使用线程,避免了频繁的创建线程,但毕竟每个客户端对应一个线程,如果服务器对应客户端很多,服务器就要创建出大量的线程,同时对服务器开销很大。

当客户端进一步增加的时候,线程数目进一步增加,系统负担越来越重,响应速度也会大打折扣。

七、⭐️

是否有办法,使用一个线程,高效处理很多客户端的并发(几万个那种)->(真正意义的高并发)

C10K:同一时刻有10k个客户端(即1w个),通过前面的一些手段和各种硬件设备,可以处理线程池类。

C10M:同时刻,有1kw的客户端并发(如某某🦌恋情),引入了很多的技术手段,其中一个必要手段之一,IO多路复用/IO多路转接(四个大字核心)

开源:引入更多的硬件设备

节流:提高单位硬件资源能够处理的请求数目。

IO多路复用——属于节流方式

例如:一家人不同的口味 饺子,包子,凉皮~~(人生不能缺少凉皮🌚🌚🌚)

我自己买,然后去别的地方买,然后再买,然后再等待,看哪个好,一起拎回去。

但是也可以同时出动一家人,他们三个去买,然后一起回来,这个相当于用了三个线程,可实际上,我一个线程不会比这三个线程慢多少。——操作系统提供的API,可搭配TCP,UDP API配合使用。


相关文章
|
1月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
105 38
|
1月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
62 2
|
1月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
74 4
|
1月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
154 2
|
18天前
|
人工智能 弹性计算 编解码
阿里云GPU云服务器性能、应用场景及收费标准和活动价格参考
GPU云服务器作为阿里云提供的一种高性能计算服务,通过结合GPU与CPU的计算能力,为用户在人工智能、高性能计算等领域提供了强大的支持。其具备覆盖范围广、超强计算能力、网络性能出色等优势,且计费方式灵活多样,能够满足不同用户的需求。目前用户购买阿里云gpu云服务器gn5 规格族(P100-16G)、gn6i 规格族(T4-16G)、gn6v 规格族(V100-16G)有优惠,本文为大家详细介绍阿里云gpu云服务器的相关性能及收费标准与最新活动价格情况,以供参考和选择。
|
23天前
|
机器学习/深度学习 人工智能 弹性计算
什么是阿里云GPU云服务器?GPU服务器优势、使用和租赁费用整理
阿里云GPU云服务器提供强大的GPU算力,适用于深度学习、科学计算、图形可视化和视频处理等多种场景。作为亚太领先的云服务提供商,阿里云的GPU云服务器具备灵活的资源配置、高安全性和易用性,支持多种计费模式,帮助企业高效应对计算密集型任务。
|
25天前
|
存储 分布式计算 固态存储
阿里云2核16G、4核32G、8核64G配置云服务器租用收费标准与活动价格参考
2核16G、8核64G、4核32G配置的云服务器处理器与内存比为1:8,这种配比的云服务器一般适用于数据分析与挖掘,Hadoop、Spark集群和数据库,缓存等内存密集型场景,因此,多为企业级用户选择。目前2核16G配置按量收费最低收费标准为0.54元/小时,按月租用标准收费标准为260.44元/1个月。4核32G配置的阿里云服务器按量收费标准最低为1.08元/小时,按月租用标准收费标准为520.88元/1个月。8核64G配置的阿里云服务器按量收费标准最低为2.17元/小时,按月租用标准收费标准为1041.77元/1个月。本文介绍这些配置的最新租用收费标准与活动价格情况,以供参考。
|
23天前
|
机器学习/深度学习 人工智能 弹性计算
阿里云GPU服务器全解析_GPU价格收费标准_GPU优势和使用说明
阿里云GPU云服务器提供强大的GPU算力,适用于深度学习、科学计算、图形可视化和视频处理等场景。作为亚太领先的云服务商,阿里云GPU云服务器具备高灵活性、易用性、容灾备份、安全性和成本效益,支持多种实例规格,满足不同业务需求。
142 2
|
1月前
|
弹性计算
阿里云2核16G服务器多少钱一年?亲测价格查询1个月和1小时收费标准
阿里云2核16G服务器提供多种ECS实例规格,内存型r8i实例1年6折优惠价为1901元,按月收费334.19元,按小时收费0.696221元。更多规格及详细报价请访问阿里云ECS页面。
66 9
|
27天前
|
监控 Ubuntu Linux
使用VSCode通过SSH远程登录阿里云Linux服务器异常崩溃
通过 VSCode 的 Remote - SSH 插件远程连接阿里云 Ubuntu 22 服务器时,会因高 CPU 使用率导致连接断开。经排查发现,VSCode 连接根目录 ".." 时会频繁调用"rg"(ripgrep)进行文件搜索,导致 CPU 负载过高。解决方法是将连接目录改为"root"(或其他具体的路径),避免不必要的文件检索,从而恢复正常连接。