网络编程套接字(socket)

1. 网络编程

指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)

2. 网络编程中的基本概念

2.1 发送端和接收端

在一次网络数据传输时:

发送端:说句的发送方进程,称为发送端,发送端主机即网络通信中的源主机

接收端:数据的接收方进程,称为接收端,接收端主机即网络通信中的目的主机

收发端:发送端和接收端两端,也简称为收发端

tip:发送端和接收端是相对的,只是一次网络数据传输产生数据流向后的概念

2.2 请求和相应

一般来说,获取一个网络资源,涉及到两次网络数据传输

  • 第一次:请求数据的发送
  • 第二次:相应数据的发送

2.3 客户端和服务端

服务端:在常见的网络数据传输场景下,把提供服务的一方进程称为服务端,可以提供对外服务

客户端:获取服务的一方进程,称为客户端

对于服务来说,一般是提供:

  • 客户端获取服务资源
  • 客户端保存资源在服务端

3. Socket 套接字

3.1 概念

Socket 套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元,基于 Socket 套接字的网络程序开发就是网络编程

3.2 分类

Socket 套接字主要针对传输层协议划分为如下三类:

1) 流套接字:使用传输层 TCP 协议

TCP,即 Transmission Contril Protocol(传输控制协议),传输层协议

TCP 特点:有连接,可靠传输,面向字节流,有接收缓冲区,也有发送缓冲区,大小不限

对于字节流来说,可以简单理解为,传输数据是基于 IO 流,流式数据的特征就是在 IO 流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收


2) 数据报套接字:使用传输层 UDP 协议

UDP,即 User Datagram Protocol(用户数据报协议),传输层协议

UDP 特点:无连接,不可靠传输,面向数据报,有接收缓冲区,无发送缓冲区,大小受限(一次最多 64k

对于数据报来说,可以简单理解为,传输数据是一块一块的,例如发送一块 100 字节的数据,必须一次发送,接收也必须一次接收 100 个字节,而不能分 100 次,每次接收 1 个字节


3) 原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据

4. 经典面试题:TCP 和 UDP 的区别

4.1 有连接/无连接

此处的连接,是“抽象”的连接,通信双方如果保存了通信对端的信息,就相当于是“有连接”,如果不保存对端的信息,就是“无连接”

4.2 可靠传输/不可靠传输

此处的“可靠”,不是指 100% 能到达对方,而是“尽可能”

相对来说,不可靠就是完全不考虑数据能否到达对方

TCP 内置了一些机制,能够保证可靠传输

  • 1) 感知到对方是不是收到了
  • 2) 重传机制,在对方没收到的时候进行重试

UDP 则没有可靠机制,完全不管发出去的数据是否顺利到达对方

4.3 面向字节流/面向数据报

TCP 是面向字节流的,TCP 的传输过程和文件流的特点是一样的

从 TCP 读写 100 个字节

1) 一次读写 100 字节

2) 2 次,每次读取 50 字节

3) 10 次,每次读取 10 字节

4) 100 次,每次读取 1 字节

UDP 面向数据报,传输数据的基本单位不是字节,而是“UDP 数据报”,一次发送/接收,必须是完整的 “UDP 数据报”

4.4 两者皆为全双工

全双工:一个通信链路,可以发送数据,也可以接收数据(双向通信)

半双工:一个通信链路,只能发送/只能接收(单向通信)

5. UDP 数据报套接字编程

5.1 API 介绍

5.1.1 DatagramSocket

DatagramSocket 是 UDP Socket,用于发送和接收 UDP 数据报

构造方法

方法名说明
DatagramSocket()创建一个 UDP 数据报套接字的 Scoket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个 UDP 数据报套接字的 Scoket,绑定到本机指定的端口(一般用于服务端)

方法

方法名说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

5.1.2 DatagramPacket

DatagramPacket 是 UDP Socket 发送和接收的数据报

构造方法

方法名说明
DatagramPacket(byte[ ] buf, int length)构造一个 DatagramPacket 用来接收数据报,接受的数据保存在字节数组(第一个参数 buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[ ] buf, int offset, int length, SocketAddress address)构造一个 DatagramPacket 用来发送数据报,发送的数据为字节数组(第一个参数 buf)中,从 0 到指定长度(第二个参数 length),address 指定目的主机的 IP 和端口号

方法

方法名说明
InetAddress getAddress()从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[ ] getData()获取数据报中的数据

构造 UDP 发送的数据报,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建

5.1.3 InetSocketAddress

构造方法

方法名说明
InetSocketAddress(InetAddress addr, int port)创建一个 Socket 地址,包含 IP 地址和端口号

5.2 编写 UDP 回显服务器

UDP Echo Server:使用 UDP 的 API 实现一个 “回显服务器”(echo server),客户端发什么要求,服务器就返回什么响应,没有任何业务逻辑,没有进行任何计算或处理

UdpEchoServer

网络编程,必须要操作网卡,就需要用到 socket 对象(DatagramSocket)

对于服务器这一端来说,需要在 socket 对象创建的时候,就指定一个端口号,作为构造方法的参数,后续服务器开始运行之后,操作系统就会把端口号和该进程关联起来

在调用这个构造方法的过程中,JVM 就会调用系统的 socket api 完成 “端口号 - 进程” 之间的关联动作

对于一个系统来说,同一时刻,一个端口号,只能被一个进程绑定,但是一个进程可以绑定多个端口号(通过创建多个 socket 对象来完成)


服务器主要的工作就是不停的处理客户端发来的请求,所以此处用一个 “死循环” 来处理

创建一个数据报对象(requestPacket 请求数据报),DatagramPacket 自身需要存储数据,但是存储数据的空间具体多大,需要外部来定义,所以每次创建数据报对象,我们传入参数(新建一个 4096 字节大小的字节数组,每次接收 4096 字节的数据)

然后通过 socket 的 receive 方法从网卡上读取数据,此处的 receive 是通过 “输出型参数”(requestPacket)获取到网卡上收到的数据的,如果网卡上收到数据了,receive 立即返回,获取收到的数据,如果网卡上没有收到数据,receive 就会阻塞等待,一直等待到真正收到数据为止


构造 String 对象是可以基于 byte[ ] 来构造的;offset 默认指的就是相对于数组开头的偏移位置,此时 offset 的值就和数组下标是等价的(从下标为 0 处开始,到数据报长度处结束)


针对 返回响应 这个操作,不能再使用上述这种空的数组来构造 Packet 对象了,我们需要根据第二步的 response 返回特定的结果

String 可以基于字节数组来构造,也可以随时取出里面的字节数组,此处通过 String 类中的 getBytes 方法取出字节数组,length 方法获取字节数组长度,单位是字节(区分: response.length() 获取字符串中 “字符” 的个数,单位是 “字符”)


UDP 有一个特点:无连接,所谓连接就是通信双方保存对方的信息(IP + 端口),DatagramSocket 这个对象中不持有对方(客户端)的 IP 和 端口,所以进行 send 的时候,需要在 send 的数据报中把 要发给谁 这样的信息写进去,才能正确的把数据进行返回


打印一下日志,方便观察


使用完毕后需要关闭,但是此代码中,socket 声明周期是跟随整个进程的,进程结束了,socket 才需要关闭,此时就算代码中没有 close,进程关闭后就会释放文件描述符表里的所有内容,也就相当于 close 了


服务器创建 socket 一定要指定端口号,端口号后客户端主动发起的时候,才能找到服务器

客户端创建 socket 最好不要指定端口号,客户端是主动的一方,不需要让服务器来找它(不指定不代表没有,客户端这边的端口号是系统自动分配了一个端口号)


UdpEchoClient


127.0.0.1 是一个特殊的IP,称为 环回IP,这个 IP 就代表本机,如果客户端和服务器在同一个主机上,就使用这个 IP

 


全部流程


全部代码:

UdpEchoServer

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

//服务端代码
public class UdpEchoServer {
    private DatagramSocket socket = null;//是 UDP Socket,用于发送和接收 UDP 数据报

    //构造方法,传入一个端口号 port
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    //通过 start 启动服务器的核心流程
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //此处通过“死循环”不停的处理客户端的请求

            //1.读取客户端的请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//创建一个4096字节大小的字节数组,每次接收4096字节的数据
            socket.receive(requestPacket);
            //上述收到的数据是二进制 byte[] 的形式体现的,后续代码要进行打印演示,所以此处转成字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            //2.根据请求计算响应,由于此处是回显服务器,响应就是请求
            String response = process(request);

            //3.把响应写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(responsePacket);

            //4.打印日志
            System.out.printf("[%s:%d] req=%s, resp=%s\n",requestPacket.getAddress(), requestPacket.getPort(), request, response);
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

UdpEchoClient

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端!");
        Scanner scanner = new Scanner(System.in);

        while (true) {
            //1.从控制台读取用户的输入
            System.out.print("-> ");
            String request = scanner.next();

            //2.构造一个 UDP 请求,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(this.serverIP), this.serverPort);
            socket.send(requestPacket);

            //3.从服务器读取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            //4.将响应打印到控制台
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

6. TCP 流套接字编程

6.1 API

6.1.1 ServerSocket

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

构造方法:

方法名说明
ServerSocket(int port)

创建一个服务端流套接字 Socket,并绑定到指定端口

tip:同一个协议下,一个端口号只能被一个进程绑定(不同协议如:9090端口在 UDP 下被一个进程绑定了,还可以在 TCP 下被另一个进程绑定

方法:

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

tip:TCP 是有连接的,有连接就需要一个 “建立连接” 的过程,建立连接的过程就类似于打电话,此处的 accept 就相当于 “接电话”(由于客户端是“主动发起”的一方,服务器是“被动接受”的一方,一定是客户端打电话,服务器接电话)


6.1.2 Socket

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

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

Socket 构造方法

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

tip:构造这个对象就是和服务器“打电话”建立连接的过程,在该构造方法中写入 IP 和端口后,意味着 new 好对象之后,和服务器的连接就建立完成了,若建立连接失败,就会直接在构造对象的时候抛出异常

Socket 方法

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

InputStream 和 OutputStream 称为 “字节流”,前面针对文件操作的方法,针对此处的 tcp socket 来说也是完全适用的

6.2 编写 TCP 回显服务器

tip:

TCP 建立连接的流程,是操作系统内核完成的,代码中感知不到

内核已经完成了建立连接的操作了,accpet 相当于是针对内核中已经建立好的连接进行“确认”操作

6.2.1 TcpEchoServer

这两个都是 socket,都是“网卡的遥控器”(操作网卡的),但是在 TCP 中使用两个不同的 socket 进行表示,分工是不同的,作用也是不同的

服务器调用一次 accept 就会产生一个新的 socket 对象来和客户端进行“一对一服务”

TCP 是全双工的通信,一个 socket 对象既可以读,也可以写

此处写入响应的时候,末尾加上 \n ,因为 TCP 是字节流的,读写方式存在无数种可能,此时就需要有办法区分出从哪里到哪里是一个完整的请求数据,所以引入分隔符来区分

读取数据的时候就隐藏了条件,请求应该是以“空白字符”(包括不限于:空格、回车、制表符、垂直制表符、翻页符...)结尾

此处就约定使用 \n 作为请求和响应结尾的标志,后续客户端也会使用 scanner 读取响应

tip:快捷键 Ctrl  Alt  t 用于快速包裹一段代码

TcpEchoServer 完整代码

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) {
        //先打印一下客户端信息
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(), clientSocket.getPort());
        //获取到 socket 中持有的流对象
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            //使用 Scanner 包装一下 inputStream 可以更方便的读取这里的请求数据
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                //1.读取请求并解析
                if (!scanner.hasNext()) {
                    //如果 scanner 无法读取出数据,说明客户端关闭了连接,导致服务器这边读取到“末尾”
                    break;
                }
                String request = scanner.next();

                //2.根据请求计算响应
                String response = process(request);

                //3.把响应写回客户端
                //此处可以按照字节数组直接写,也可以使用 PrintWriter 写
                //outputStream.write(response.getBytes());
                printWriter.println(response);

                //4.打印日志
                System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);

                System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

6.2.2 TcpEchoClient 完整代码

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.SQLOutput;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");

        try (InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                //1.从控制台读取数据
                System.out.println("-> ");
                String request = scannerIn.next();
                //2.把请求发送给服务器
                printWriter.println(request);
                //3.从服务器读取响应
                if (!scanner.hasNext()) {
                    break;
                }
                String response = scanner.next();
                //4.打印响应结果
                System.out.println(response);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

6.2.3 上述代码三大 bug 之一:客户端无法发送请求

原因:

PrintWriter 这样 IO 流中的类都是自带“缓冲区”的,引入缓冲区后,进行写入数据的操作不会立即触发 IO,而是先放到内存缓冲区中,等到缓冲区里攒了一波之后再统一发送

由于代码中此处的数据较少,无法攒一波进行发送,因此数据便一直停留在缓冲区中,出不去了

为解决这个问题,引入 flush 操作,主动“刷新缓冲区”

运行结果:


6.2.4 bug 之二:当前服务器代码针对 clientSocket 没有进行 close 操作

像 ServerSocket、DatagramSocket 他们的生命周期都是跟随整个进程的

而这里的 clientSocket 是“连接级别”的数据,随着客户端断开连接,这个 socket 也就不再使用了(即使是同一个客户端,断开之后重新连接,也是一个新 socket ,和旧 socket 不是同一个了),因此,这样的 socket 就应该主动关闭,防止文件资源泄露

解决方案:在 finally 中加入 close 操作


6.2.5 bug 之三:当前代码无法对多个客户端提供服务

作为一个服务器,就是要同时给多个客户端提供服务的(当然现在也有专属服务器,但大部分不是)

本程序目前同一时刻只能给一个客户端提供服务,停止上一个客户端才能服务下一个客户端,这是不科学的

想要同时启动多个客户端,需要修改一些配置:

当启动多个客户端后,分别在两个客户端中输入,发现服务器只能响应一个客户端:

当把第一个客户端关掉后,第二个客户端瞬间得到了刚才没有得到响应的所有结果

这是因为当第一个客户端连上服务器之后,服务器代码就会进入 processConnect 内部的 while 循环,此时第二个客户端尝试连接的时候,无法执行到第二次 accept,所有第二个客户端发来的请求数据都积压在操作系统内核的接收缓冲区中,第一个客户端退出的时候,processConnection 的循环就结束了,于是外层循环就可以执行 accept 了,然后就可以处理第二个客户端之前积压的请求数据了

这里无法处理多个客户端,本质上是服务器代码结构存在问题,采取了双重 while 循环的写法,就会导致进入内层 while 后,外层 while 无法继续执行了

6.3 服务器引入多线程

解决方案:把双重 while 改为 一重 while,分别进行执行(多线程)

修改后 TcpEchoServer

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();
            Thread t = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

    //针对一个连接,进行逻辑处理
    private void processConnection(Socket clientSocket) throws IOException {
        //先打印一下客户端信息
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(), clientSocket.getPort());
        //获取到 socket 中持有的流对象
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            //使用 Scanner 包装一下 inputStream 可以更方便的读取这里的请求数据
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                //1.读取请求并解析
                if (!scanner.hasNext()) {
                    //如果 scanner 无法读取出数据,说明客户端关闭了连接,导致服务器这边读取到“末尾”
                    break;
                }
                String request = scanner.next();

                //2.根据请求计算响应
                String response = process(request);

                //3.把响应写回客户端
                //此处可以按照字节数组直接写,也可以使用 PrintWriter 写
                //outputStream.write(response.getBytes());
                printWriter.println(response);
                printWriter.flush();

                //4.打印日志
                System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

修改后 TcpEchoClient

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.SQLOutput;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");

        try (InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                //1.从控制台读取数据
                System.out.print("-> ");
                String request = scannerIn.next();
                //2.把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();

                //3.从服务器读取响应
                if (!scanner.hasNext()) {
                    break;
                }
                String response = scanner.next();
                //4.打印响应结果
                System.out.println(response);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

6.3 服务器引入线程池

上述的代码属于比较经典的一种服务器开发模型,给每个客户端分配一个线程来提供服务

但是一旦 短时间内有大量的客户端,并且每个客户端发一个请求之后就快速断开连接了,这时就会对服务器有比较大的压力

为解决上述问题,就引入了线程池

线程池本质上也是一个线程服务于一个客户端,但使用线程池就是在复用线程,当客户端连接创建好线程后,客户端断开连接,线程不会销毁,继续复用

6.4 重点补充:

1) 线程池的选择

此处使用这个线程池是因为他的最大线程数是 21亿

若使用下面的线程池,则必须要指定固定线程数,意味着同时只能处理这么多个客户端

2) IO 多路复用

当短时间内有大量客户端,并且客户端持续的发送请求处理响应,连接会保持很久,这种场景就不能使用 多线程/线程池了,因为对于一个系统来说,搞几百个线程压力就非常大了

针对这个问题,就要使用 IO 多路复用,即使客户端的数据非常多,仍然可以使用较少的线程提供高效的服务

原理分析:

虽然上述代码中是一个线程服务于一个客户端,但实际上每个这样的线程都可能会阻塞(客户端也不是持续的发送请求的),相比于处理请求的时间,大部分的时间可能都是在阻塞等待,因此可以让一个线程同时给多个客户端提供服务

比如,给一个线程分配 1000 个客户端进行处理,同一时刻,可能 1000 个客户端里之后几十个客户端需要处理请求

针对上述的情况,就需要操作系统内部提供支持,IO 多路复用就是操作系统内核提供的功能(IO 多路复用具体的实现方案有多种,最知名的就是 Linux 下的 epoll)

3) 流对象的关闭问题

首先要明确:调用 close 主要目的是为了释放文件描述符,其中的 InputStream 和 OutputStream 是持有文件描述符的,所以需要 close,而 Scanner 和 PrintWriter 只是持有 InputStream 和 OutputStream,并不持有文件描述符,因此此处无需 close

tip:一个进程中,有三个特殊的流对象(特殊的文件描述符)不需要关闭

这三个流对象是进程一启动,操作系统就自动打开的,他们的生命周期是要跟随整个进程的

  • System.in => 标准输出
  • System.out => 标准输出
  • System.err => 标准错误

7. 长短连接

长连接:客户端连上服务器之后,一个连接中会多次发起请求,接收多个响应(一个连接到底进行多少次请求,不确定)

短链接:客户端连上服务器之后,一个连接只发一个请求,接收一个响应,然后就断开连接了

两者区别如下:

建立连接、关闭连接的耗时:短链接每次请求、响应都需要建立连接、关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,所以长连接效率更高

主动发送请求不同:短链接一般是客户端主动向服务器发送请求;而长连接可以是客户端主动发送请求,也可以是服务器主动发

两者的使用场景不同:短链接适用于客户端请求频率不高的场景,如浏览网页等;长连接适用于客户端与服务器通信频繁的场景,如聊天室、实时游戏等

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值