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配合使用。


相关文章
|
17天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
18天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
30天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
1月前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
58 0
|
3天前
|
安全 算法 Java
JavaSE&多线程&线程池
JavaSE&多线程&线程池
17 7
|
4天前
|
存储 缓存 NoSQL
为什么Redis使用单线程 性能会优于多线程?
在计算机领域,性能一直都是一个关键的话题。无论是应用开发还是系统优化,我们都需要关注如何在有限的资源下,实现最大程度的性能提升。Redis,作为一款高性能的开源内存数据库,因其出色的单线程性能而备受瞩目。那么,为什么Redis使用单线程性能会优于多线程呢?
17 1
|
20天前
|
Python
Python网络编程基础(Socket编程)UDP服务器编程
【4月更文挑战第8天】Python UDP服务器编程使用socket库创建UDP套接字,绑定到特定地址(如localhost:8000),通过`recvfrom`接收客户端数据报,显示数据长度、地址和内容。无连接的UDP协议使得服务器无法主动发送数据,通常需应用层实现请求-响应机制。当完成时,用`close`关闭套接字。
|
26天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1
|
26天前
|
JavaScript 前端开发
JS 单线程还是多线程,如何显示异步操作
JS 单线程还是多线程,如何显示异步操作
22 2
|
1月前
|
消息中间件 Java 数据库连接
【C++ 多线程】C++ 多线程环境下的资源管理:深入理解与应用
【C++ 多线程】C++ 多线程环境下的资源管理:深入理解与应用
38 1