Java EE:6.网络编程套接字(第二弹)

目录

4.TCP流套接字编程

[4.1 API介绍](#4.1 API介绍)

ServerSocket

Socket

[4.2 TCP版本服务器代码示例](#4.2 TCP版本服务器代码示例)

[TCP Echo Server(服务端)](#TCP Echo Server(服务端))

[Socket vs ServerSocket](#Socket vs ServerSocket)

到目前为止的代码

答疑

[TCP Echo Client(客户端)](#TCP Echo Client(客户端))

到目前为止的代码

服务端与客户端的交互

第一个玄机:

第二个玄机:

第三个玄机:

这是为什么呢??

服务器引入多线程

服务器引入线程池

完整TCP服务端代码

完整TCP客户端代码

长短连接(课件内容)

扩展了解


书接上文:Java EE:6.网络编程套接字(第一弹)~~

闲聊:

Q1:计算机二级过了!!

恭喜恭喜,但是后面做简历的时候,千万不要往简历上写就好了~~(简历上是减分项)

二级是非常好过的~~

相当于你应聘翻译,你简历上写熟悉 a-z 26个字母的拼写

Q2:驾照需要写吗?

也不是很需要~~

Q3:学校老师说软考越来越重要?

纯属扯淡~~

Q4:软考对简历怎么样??

软考考的东西和你日常开发工作用的东西,没有半点联系

软考虽然比计算机二级难一点,写简历上不是扣分项,但也绝对不是加分项,不加不减~~

软考相当于把计算机中涉及到的各个学科(不止开发和数据结构,还有操作系统、网络、法律条文、软件工程、工程管理......),每个学科挑一些基础的知识点,考你~~

而且软考的题,技巧性非常强,只有你掌握了技巧,哪怕你那些知识啥都不懂,你也能做的七七八八~~

比较务虚,哪怕你考的总分130、140,也不见得你代码就能写的好~~

45+45算过,报名费好像两百多呢~~要是想体会一下考证啥感觉可以去,要是想通过这个写简历上提升竞争力,那还是想多了~~

4.TCP流套接字编程

在 TCP 这里,ServerSocket 和 Socket 都表示 Socket

ServerSocket:专门给服务器用的

Socket:服务器和客户端都会用

那在 TCP 这里不需要一个类来表示发送的基本单位吗??

TCP 的一个核心特点:面向字节流

读写数据的基本单位就是 byte,就不需要一个专门的类了,直接使用字节表示即可

4.1 API介绍

ServerSocket

ServerSocket 是创建 TCP 服务端 Socket 的API

ServerSocket 构造方法:

|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务器流套接字Socket,并绑定到指定端口 |

服务器启动,需要先绑定端口号~~

ServerSocket 方法

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

TCP 和 UDP 不同,TCP 是"有连接",这里的 accept 就是联通连接的关键操作~~

Socket

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

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

Socket 构造方法:

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

这两个参数是服务器的 IP 和 服务器的端口

Socket 方法:

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

TCP 这里没有 send / receive 操作,而是借助字节流对象来完成

4.2 TCP版本服务器代码示例

TCP Echo Server(服务端)

java 复制代码
package network;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    //先创建一个 serverSocket 对象
    private ServerSocket serverSocket=null;
    //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    
    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            Socket clientSocket=serverSocket.accept();
        }
    }
}

连接可以理解为"打电话"

一边拨号,另一边要接通才能正常进行通信

而咱们要做的就是"接通"的操作(accept),而"拨号"的操作,则是客户端来完成的~~

Socket vs ServerSocket

比如说我有一次买房子,十一假期的时候,在马路牙子上遇见一个西装革履的小哥~~

后来发现他是个卖房子的销售,而我恰好有买房子的需求,于是就带着我去了售楼部,售楼部里人山人海~~

闲聊:后来才发现,很多售楼部都是人山人海,其中很多都是演员(托)~~

小哥一挥手,来了一个同样西装革履的小姐姐~~OL风格~~

小哥跟我说,她是专业的置业顾问(销售),由她来给你介绍咱们楼盘详细情况~~

而小哥一转身,就消失在茫茫人海中了~~

这个过程中:

小哥:负责在马路牙子上揽客~~外场

就是 ServerSocket 做的工作~~

小姐姐:给客人提供详细的服务~~内场

就是 Socket 做的工作~~

闲聊:

为啥不是小姐姐揽客??

都有都有~~

当时我吧就真的把这个房子给买了~~

但是这是个悲伤的故事,开发商是 恒大~~

2017年,恒大是最牛逼的开放商

但是2019年,恒大就暴雷了,导致很多楼盘,纷纷烂尾~~

当时我已经拿到房子了,只是没有房本~~

后来国家入场了,专门拨款来建设烂尾楼~~

房本也就拿到了~~

所以买房的时候,还是建议大家买"现房",虽然"期房"能便宜十几万,但是很容易烂尾~~亏的就太大了~~

java 复制代码
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    //先创建一个 serverSocket 对象
    private ServerSocket serverSocket=null;
    //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            //通过读写 clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时 accept 就会阻塞
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    //处理一个客户端的连接
    //可能要涉及到多个客户端的请求和响应
    private void processConnection(Socket clientSocket){
        //打印客户端的地址和端口号
        System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //后面读取请求的时候就写到输入流对象,然后写入输出流对象
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            //分成三个步骤
            while(true){
                //1.读取请求并解析
                byte[] request=new byte[1024];
                InputStream.read(request);
                //2.根据请求计算响应
                //3.返回响应到客户端
            }
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }
}

写到这里时,读取请求并解析的过程可以搞一个字节数组,然后通过 InputStream 来读(文件操作讲过的内容),但是这样写的话,读到的还是字节数组,还是需要我们手动把字节数组转成 String,才方便去进行后续处理,其实我们还有一个更简单的方法:借助 Scanner 来完成读取操作,读的同时还是一个 String

Scanner 这个东西,非常好用,不仅仅可以处理控制台的输入,还能控制文件的输入,还可以控制网络的输入~~

Scanner 构造方法,填入的其实是一个 InputStream 对象~~

1.往 Scanner 里填 System.in →从控制台读

2.往 Scanner 里填 FileInputStream →从文件读

3.往 Scanner 里填 Socket 获取到的 InputStream →从网络读

因此通过 Scanner 获取输入还是非常方便的~~

到目前为止的代码
java 复制代码
package network;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class TcpEchoServer {
    //先创建一个 serverSocket 对象
    private ServerSocket serverSocket=null;
    //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            //通过读写 clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时 accept 就会阻塞
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    //处理一个客户端的连接
    //可能要涉及到多个客户端的请求和响应
    private void processConnection(Socket clientSocket){
        //打印客户端的地址和端口号
        System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //后面读取请求的时候就写到输入流对象,然后写入输出流对象
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            //针对 InputStream 套了一层
            Scanner sc=new Scanner(inputStream);
            //针对 OutputStream 套了一层
            PrintWriter writer=new PrintWriter(outputStream);
            //分成三个步骤
            while(true){
                //1.读取请求并解析,可以直接 read,也可以借助 Scanner 来辅助完成
                if(!sc.hasNext()){
                    //没有下一个数据可以读了,连接断开了
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;//结束循环
                }
                String request=sc.next();
                //2.根据请求计算响应
                String response=process(request);
                //3.返回响应到客户端
                //按照字节的方式填写
                //outputStream.write(response.getBytes());
                //也可以使用字符流的方式套一层
                //此时 writer 就和之前说的 System.out 是一样的效果了
                writer.println(response);

                //打印日志
                System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }
        }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();
    }
}

大体流程与 UDP 相比就是多了一个针对连接的处理

连接建立好之后就到了文件 IO 里面的内容一样

拿着 IO 流进行操作,只不过操作的时候,我们稍微套了一下,用 Scanner 和 PrintWriter 这两个类来去代表原来的 inputStream 和 outputStream,主要是因为我们当前是按照字符串来处理文本,这个时候,我们自然而然使用字符流来进行处理,包括说使用 Scanner 来处理,是更方便,更简单的做法

答疑

Q1:一次请求就相当于一次连接吗??

一个连接中,可以包含若干次请求~~

打电话的时候,打通一次电话,可以只说一句话就挂了👇

短链接:一个连接中只有一个请求

也可能收很多句话再挂~~👇

长连接:一个连接中有多个请求

日常开发中,长连接是更常见的情况,因为短链接太消耗性能了,每次发请求都要先建立连接,就像之前我们学过的锁消除一样,给领导汇报工作,应该一口气把该说的话给说完

Q2:意思就是客户端可以发一些文字,然后他回应了,继续发?

发送多次请求,拿到多次响应,不局限于字符串~~(只不过当前咱们这个代码只处理了字符串)

TCP Echo Client(客户端)

java 复制代码
package network;

import java.io.IOException;
import java.net.Socket;

public class TcpEchoClient {
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //直接把字符串的 IP 地址,设置进来
        //类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~
        socket=new Socket(serverIp,serverPort);
    }
}

创建 socket 对象就会在底层和对端建立 tcp 的连接(所谓连接,就是记录了对端的信息)

服务器的 IP 和 端口 这样的信息,就不需要自己创建变量保存了,直接 TCP 内部就记住了

这一点和 UDP 还不一样,当时我们写 UDP 的时候是自己创建了两个变量来保存服务器的 IP 和 端口 👇

而对于 TCP 来说,你自己创建变量保存也不是不可以,但是没必要~~

到目前为止的代码
java 复制代码
package network;

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

public class TcpEchoClient {
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //直接把字符串的 IP 地址,设置进来
        //类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~
        socket=new Socket(serverIp,serverPort);
    }

    public void start(){
        Scanner sc=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()) {
            //为了使用方便,来个套壳操作
            Scanner scanner=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);
            //从控制台读取请求,发送给服务器
            while(true){
                //1.从控制台读取用户输入
                String request=sc.next();
                //2.发送给服务器
                writer.println(request);
                //3.读取服务器返回的响应
                String response=scanner.next();
                //4.打印到控制台
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

服务端与客户端的交互

与 UDP 类似,这里的服务端和客户端也存在着配合👇

需要注意的是,服务端的 socket 对象👇

java 复制代码
private ServerSocket serverSocket=null;

和客户端的 socket 对象👇

java 复制代码
private Socket socket=null;

这俩 socket 对象,绝对不是同一个对象(分别在不同进程中,甚至在不同主机上)

这俩 socket 对象可以理解为两部电话

从 A 的话筒说话,B 的听筒就能听见

从 B 的话筒说话,A 的听筒就能听见

这个过程,A 和 B 在进行一次通信,但 A 和 B 绝对不是同一个对象!!

但是我们运行上述服务端和客户端两个代码之后,会发现客户端已经发了请求,但是服务器并没有收到👇

TCP回显服务端与客户端的交互出现的问题


答疑:客户端服务器,用过一个的端口号,有时候下一次这个端口号用不了了,换一个端口号又可以了,为什么??

??肯定是可以用的,但是前提是你需要把前一个服务器给停掉,因为一个端口只能被一个端口给绑定👇

一个端口只能被一个进程绑定


其实不是服务器没收到,而是客户端的请求根本没有发出去~~

问题就出在客户端的这行代码上👇

java 复制代码
//2.发送给服务器
writer.println(request);

这个操作只是把数据放到"发送缓冲区"中,还没有真正写入到网卡里

这个"发送缓冲区"我们之前也说过(嗑瓜子皮儿的那个例子),本质就是一段内存空间

此时我们就需要用 flush 方法来"冲刷缓冲区",将缓冲区中的内容强制发送出去👇

flush刷新缓冲区

答疑:这是 println 的行为吗?如果我不套壳,直接使用 write(),write()也只是放到缓冲区吗??

这是 PrintWriter 的行为,如果不套壳,是可以直接发送的

实际开发中,广泛使用缓冲区这样的概念,flush 这个操作是很关键的~~


上述代码看起来确实能够运行进行通信了,但其实还"暗藏玄机"~~

第一个玄机:

在客户端的下面这行代码中的行为,是自动加上 \n 的👇

java 复制代码
//2.发送给服务器
writer.println(request);

如果我们把 println 改成 print 呢?会怎样??👇

第一个玄机:换行符

我们发现,又出现了之前的情况,貌似还是没发出去~~

但实际上,这个时候是发过去了的,服务器是收到了的,但是服务器没有真正处理~~

为啥??问题就在服务器代码中的这一行👇

java 复制代码
if(!sc.hasNext()){

hasNext() 会判定收到的数据中是否包含"空白符"(换行、回车、空格、制表符、翻页符......),遇到空白符,才认为是一个"完整的 next ",在遇到之前,都会阻塞

而下面这行代码👇其实就是在暗暗约定:一个请求 / 响应 使用 \n 作为结束标记,对端读的时候,也是读到 \n 就结束(认为是读到一个完整的请求了)

java 复制代码
//2.发送给服务器
writer.println(request);

在 UDP 中,就是以 DatagramPacket 作为单位的

而 TCP 则是以 字节为单位,实际上一个请求,往往是由多个字节构成的,所以引入分隔符是我们标记完整请求边界的一种典型方式,不一定是换行,也可以是其他的、你所约定的任意的字符,总之,我们要通过这种方式,把请求与请求之间的边界给划分好,这个也是 TCP 中的一个关键的环节,后续我们讲 TCP 原理的时候再进一步展开,这里其实涉及到一个"粘包问题"~~

第二个玄机:

在服务器代码中我们涉及到两种 socket ,其中 serverSocket 生命周期贯穿我们整个服务器进程,肯定不需要对它进行关闭,但是 clientSocket 还不关闭,还可以吗??

这里 clientSocket 我们分析一下可以发现,它的生命周期并不是贯穿整个进程,而是跟随这一次连接了👇

但是我们代码中并没有写释放,没写 close() 意味着这里就会产生文件资源泄露,只有打开没有关闭,打开太多就会把文件描述符表耗尽,也就没办法打开新的了

解决方案很简单:在整个方法执行完,在 try - catch 后面加上一个 finally ,补上 close(),然后解决一下异常就行了👇

第三个玄机:

我们知道,一个服务器要能同时给多个客户端提供服务~~

但是我们上面写的代码貌似有点小问题~~

由于 IDEA 默认同一时刻同样的进程只能运行一个,但此时我们需要模拟多个客户端,因此需要设置一下允许运行多个实例👇

接下来我们运行代码看效果👇

第三个玄机:同一时刻只能处理一个客户端

我们发现,问题很明显,同一时刻只能处理一个客户端

这是为什么呢??

其实是代码结构不合理、有 Bug~~

问题就出现在这里:无法同时等待 accept 和等待用户请求

等待用户发请求的时候,没法等 accept

这个时候,有新的客户端连过来了,也无法接通电话

这就导致了同一时刻,服务器只能处理一个客户端的请求了

服务器引入多线程

那怎样兵分两路,分别处理呢??多线程~~

线程的诞生,就是为了这个场景服务的~~

主线程负责进行 accept ,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求

类似于一个负责在外面揽客,一个负责内场的销售小姐姐负责介绍~~

代码改动如下👇

java 复制代码
    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            //通过读写 clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时 accept 就会阻塞
            
            //主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求
            Socket clientSocket=serverSocket.accept();
            Thread t=new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

每个新线程负责去处理一个客户端的请求和响应,直至客户端断开了,这个线程也就随之结束了

看看效果👇

使用多线程解决同一时刻只能处理一个客户端的问题

我们在 jconsole.exe 可以看见这个四个线程的状态:一个主线程和三个子线程

其中三个子线程都在这里阻塞👇等待有客户端发送新的请求

java 复制代码
if(!sc.hasNext()){//我们发现三个子线程都是阻塞在这个位置

主线程在这里阻塞👇等待客户端建立连接

java 复制代码
Socket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~
服务器引入线程池

有细心的老铁应该能发现,此时有客户端上来,就创建新的线程,客户端断开,线程就销毁了,是否有办法进一步优化一下呢??

答疑:线程安全??

咱们只是读一下就打印,不涉及到修改同一个变量,因此不涉及线程安全~~

有老铁就想到了,线程池~~

我们可以使用线程池替换掉频繁创建线程的逻辑👇

java 复制代码
    public void start() throws IOException {
        System.out.println("启动服务器");
        
        //这种情况一般不会使用 fixedThreadPool,意味着同时处理的客户端连接数目就固定了
        ExecutorService executorService= Executors.newCachedThreadPool();
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            //通过读写 clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时 accept 就会阻塞

            //主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求
            Socket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~
            //使用多线程的方式来调整
//            Thread t=new Thread(()->{
//                processConnection(clientSocket);
//            });
//            t.start();
            
            //使用线程池来调整
            executorService.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

提出问题:不论多线程还是线程池,都意味着一个客户端对应一个线程,那么一个主机上创建的线程数目是否有上限呢??有的!!

正常一个主机创建几千个线程,就已经不容易了,再想创建几万个线程,几十万个线程,是不靠谱的~~

正如我们之前将多线程的时候也说过,线程太多了,也没啥效率了~~

在2000年左右的时候,这个问题叫 C10K 问题:1w个客户端怎么办??

后来随着技术发展,解决 C10K 已经不满足了,引发了 C10M 问题:1000w个客户端怎么办??

其实也有解决办法:IO 多路复用(IO 多路转接)

由于现在的 JVM 中没有原生的 IO 多路复用 API,而是把 API 重新封装了,已经不仅仅是 IO 多路复用了,咱们这里就不展开介绍了,这是隔壁C++需要重点讲的内容

后续我们在专门的精品课部分会详细讲这一块:IO 多路复用、Java NIO、Netty 知名网络框架......

其实当前学的 IO 也叫 BIO(阻塞 IO,Blocking IO)

NIO(非阻塞 IO,NO Blocking IO)

答疑:这个阻塞和 WAITING 有什么区别??

这俩玩楞没有关联关系~~

完整TCP服务端代码

java 复制代码
package network;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    //先创建一个 serverSocket 对象
    private ServerSocket serverSocket=null;
    //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");
        
        //这种情况一般不会使用 fixedThreadPool,意味着同时处理的客户端连接数目就固定了
        ExecutorService executorService= Executors.newCachedThreadPool();
        while(true){
            //对于 TCP 来说,需要先处理客户端发来的连接
            //通过读写 clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时 accept 就会阻塞

            //主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求
            Socket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~
            //使用多线程的方式来调整
//            Thread t=new Thread(()->{
//                processConnection(clientSocket);
//            });
//            t.start();
            
            //使用线程池来调整
            executorService.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

    //处理一个客户端的连接
    //可能要涉及到多个客户端的请求和响应
    private void processConnection(Socket clientSocket){
        //打印客户端的地址和端口号
        System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //后面读取请求的时候就写到输入流对象,然后写入输出流对象
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            //针对 InputStream 套了一层
            Scanner sc=new Scanner(inputStream);
            //针对 OutputStream 套了一层
            PrintWriter writer=new PrintWriter(outputStream);
            //分成三个步骤
            while(true){
                //1.读取请求并解析,可以直接 read,也可以借助 Scanner 来辅助完成
                if(!sc.hasNext()){//我们发现三个子线程都是阻塞在这个位置
                    //没有下一个数据可以读了,连接断开了
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;//结束循环
                }
                String request=sc.next();
                //2.根据请求计算响应
                String response=process(request);
                //3.返回响应到客户端
                //按照字节的方式填写
                //outputStream.write(response.getBytes());
                //也可以使用字符流的方式套一层
                //此时 writer 就和之前说的 System.out 是一样的效果了
                writer.println(response);
                //这里也需要刷新缓冲区
                writer.flush();
                //打印日志
                System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }
        }catch (IOException e){
            throw new RuntimeException(e);
        }finally {
            try {
                clientSocket.close();
            } 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();
    }
}

完整TCP客户端代码

java 复制代码
package network;

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

public class TcpEchoClient {
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //直接把字符串的 IP 地址,设置进来
        //类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~
        socket=new Socket(serverIp,serverPort);
    }

    public void start(){
        Scanner sc=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()) {
            //为了使用方便,来个套壳操作
            Scanner scanner=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);
            //从控制台读取请求,发送给服务器
            while(true){
                //1.从控制台读取用户输入
                String request=sc.next();
                //2.发送给服务器
                writer.println(request);
                //  加上刷新缓冲区操作,才是真正发送数据
                writer.flush();
                //3.读取服务器返回的响应
                String response=scanner.next();
                //4.打印到控制台
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

长短连接(课件内容)

TCP 发送数据时,需要先建立连接,什么时候关闭连接就决定是短链接还是长连接:

短链接:每次接收到数据并返回响应后,都关闭连接,即是短链接,也就是说,短链接只能一次收发数据

长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接,也就是说,长连接可以多次收发数据

对比以上长短连接,两者区别如下:

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

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

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

扩展了解

基于 BIO(同步阻塞 IO)的长连接会一直占用系统资源,对于并发要求很高的服务端系统来说,这样的消耗是不能承受的

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行

一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求

实际应用时,服务端一般时基于 NIO(即同步非阻塞 IO)来实现长连接,性能可以极大的提升

相关推荐
做一个快乐的小傻瓜1 小时前
ZYNQ DEV套件引脚约束
java·linux·运维
devilnumber1 小时前
Java Lambda 表达式 200 条常见问题、坑点、易错点、规范清单
java·开发语言
极客先躯1 小时前
高级java每日一道面试题-2026年02月12日-实战篇[Docker]-什么是容器的 Seccomp 配置?如何自定义?
java·运维·分布式·docker·容器·自动化·文件
大大杰哥1 小时前
Vue2学习(3)--组件中的通信方式/组件之间的交互
java·前端·javascript
zzz_23681 小时前
【Java基础】二叉树遍历与红黑树的完美平衡艺术——从递归崩溃到自平衡的硬核拆解
java·开发语言
网络研究院1 小时前
美国网络安全法律与实践
网络·安全·美国·法律·实践
yyuuuzz1 小时前
云服务器软件部署的常见问题与经验
linux·运维·服务器·网络·数据库·人工智能·github
斯内普吖1 小时前
(开源)高校素拓分管理系统小程序实战指南 基于 Java + SpringBoot + uni-app + Vue + MySQL
java·spring boot·mysql·小程序·uni-app·开源
橘子星1 小时前
树与二叉树:从概念到 JavaScript 实现
前端·javascript·面试