8/24 Java学习博客

网络编程

网络编程就是通过网络,让两个主机之间能够进行通信,基于这样的通信来完成一定的功能。

我们在进行网络编程的时候,需要操作系统给咱们提供一组API,通过这些API才能完成编程。

网络编程里面我们就使用SocketAPI来完成不同主机之间,不同系统之间的网络通信。

API可以认为是应用层和传输层之间交互的路径

在传输层中 提供了主要的两个网络协议 TCP和UDP
这两个协议的特性(工作原理)差异很大,导致我们使用这两种协议进行网络编程,也存在一定的差异,系统就分别提供了两套API。

一、那首先就是对TCP和UDP之间的差异进行个认识

1.TCP是有连接的,UDP是无连接的
(连接是一种抽象的概念)此处连接的本质就是建立连接的双方,各自保存对方的信息。

TCP想要通信,就需要先建立连接,做完之后才能进行后续通信(如果A想和B建立连接,B拒绝了,则通信无法完成。)

UDP想要通信,就直接发送数据即可,不需要征得对方的同意,UDP自身也不会保存对方的信息。

UDP不知道,但是写程序的人得知道,UDP自己不保存,但是我们调用UDP的socketAPI时候要把对方的位置啥的个传过去。

2.TCP是可靠传输,UDP是不可靠传输

不过这里的可靠不是说100%传达 !
而是说 可靠传输 是一种退而求其次
A——>B发消息时,消息有没有到达B这里,A是能知道的,进一步的就可以再发送失败时候采取一定措施(尝试重传之类的)

那这里就说 不是用可靠传输更好吗
可靠传输有两个缺点:(1)机制更复杂(2)传输效率更低

3.TCP是面向字节流的,UDP是面向数据报的
此处说的字节流,跟文件操作里面的字节流是一个意思!
TCP也是和文件操作一样,以字节位单位来进行传输

UDP则是按照数据报为单位,来进行传输。 UDP数据报也是有严格的格式的。

4.TCP和UDP都是全双工的
一个信道,允许双向通信,就是全双工
一个信道,只能单向通信。就是半双工
代码中就是使用一个socket对象,就可以发送数据也可以接受数据。

什么是Socket

课上老师说是网卡抽象出来的文件,那实际上,我们应该把它理解成系统提供给程序的”网络通信端点“,而不是网卡抽象成文件。

其实Socket 和网卡实际的链路更像

Java 程序

DatagramSocket

操作系统 Socket

UDP

IP

网卡驱动

网卡

网络

“抽象成文件”这个想法,其实来源于 Unix/Linux 的“一切皆文件”思想。
在 Linux中,Socket 对象可以通过文件描述符 fd 来访问,所以使用方式有点像文件。

所以 Socket 抽象的是网络通信接口/通信端点,而不是网卡本身。

二、学习UDP的socketAPI如何使用

主要用到两个

(1)DatagramSocket
使用这个类来表示一个 UDP 通信端点,主要负责 UDP 数据报的发送和接收。
UDP 是无连接的,因此通常直接通过 send() 和 receive() 进行通信。

(2)**DatagramPacket **
使用这一个类,来表示一个UDP数据报,UDP是面向数据报的,所以每次进行传输都要以UDP数据报为基本单位。

我们就写一个简单的UDP的客户端/服务器通信的程序.这个程序没有啥业务逻辑,只是单纯的调用socket api.
让客户端给服务器发送一个请求,请求就是一个从控制台输入的字符串.
服务器收到字符串之后,也就会把这个字符串原封不动的返回给客户端,客户端再显示出来.

也就是回显服务器

首先我们进行编写
服务端UdpEchoServer

开始肯定是要创建 DatagramSocket对象 也是后续进网络传输的基础

private DatagramSocket socket = null;

接着写下构造方法 需要注意的是 服务器的socket一般要显式的指定一个端口号.


 public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

然后接着写启动方法
服务器启动后 第一步就是读取请求并解析 读取请求我们就是用到receive()

那这个receive方法要求里面有一个参数DatagramPacket p 那这里的参数也是一个输出型参数 跟文件内容操作里的read()方法里的char[] cbuf 数组是一样的,都是输出型参数

一个服务器程序,经常是要长时间运行的(7*24)也不知道客户端啥时候来.先构造一个空着的对象,然后传递到方法内部.由receive 内部对这个数据进行填充

那我们肯定就是需要初始化个DatagramPacket p

这里我们就命名为 requestPacket

DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

这个对象用来承载从这边读到的数据.收到数据的时候,需要搞一个内存空间来保存这个数据DatagramPacket 内部不能自行分配内存空间因此就需要程序员手动把空间创建好,交给 DatagramPacket进行处理~然后再 socket.receive(requestPacket);

服务器一旦启动,就会立即执行到这里的receive方法.此时,客户端的请求可能还没来呢
这种情况也没关系.receive就会直接阻塞.就会一直阻塞到真正客户端把请求发过来为止.

完成receive后数据以二进制的形式存储到DatagramPacket底下,但是要想能够把这里的数据给显示出来,我们还需要把这个二进制的数据转化成字符串

String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

这个getLength得到的这个结果是收到的数据的真实长度.
0, requestPacket.getLength() 取这个区间内的字节,构造成一个String

转换完后我们还需要根据请求计算响应,由于我们这里是回显服务器所以就直接返回一样的就可以了

 String response = process(request);

public String process(String request) {
        return request;

    }

然后把响应写回客户端,搞一个响应对象 DatagramPacket,往里面构造数据通过send返回

 DatagramPacket responsePacket = new  DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());

UDP无连接的(UDP自身不会保存数据要发给谁),就需要每次发送的时候,重新指定,数据要发到哪里去!!我们就构造这个数据报,需要指定数据内容,也指定一下数据报要发给谁发给的就是requestPacket.getSocketAddress()里获取的地址

把这个代码换成response.getBytes().length
response.length()是否可以?

本质上是跟字符集有关系,如果你这个学符串里内容都是英文学符,此时字节和字符个数是一样的.如果包含中文就不一样了。

在进行网络传输的时候,肯定是要使用字节的!!
然后
socket.send(responsePacket);

那为了不断去接受数据报和响应我们再在外面做一个while循环即可

最后再写一个main方法启动下就可以了

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

        server.start();
    }

最后完整的服务端

package network;

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

public class UdpEchoServer {
    //创建一个DatagramSocket对象 后续操作网卡的基础
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }


    public void start() throws IOException {
        //服务器启动
        System.out.println("服务启动");

        while(true){
            //1.读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //完成receive后数据以二进制的形式存储到DatagramPacket底下
            //要想能够把这里的数据给显示出来,还需要把这个二进制的数据转化成字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //2.根据请求计算响应(一般服务器都会经历的过程)
            //由于此处是回显服务器,请求是啥样,响应就是啥样
            String response = process(request);
            //3.把响应写回客户端
            //搞一个响应对象 DatagramPacket
            //往里面构造数据通过send返回
            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().toString(),requestPacket.getPort(), request, response);


        }
    }

    public String process(String request) {
        return request;

    }

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

        server.start();
    }

}

接着实现客户端

那开始我们也是先创建DatagramSocket对象 不过此外我们还要先初始化下IP和端口

private DatagramSocket socket = null;
    private String serverIp = "";
    private int serverPort = 0;

接着再来构造方法

 public UdpEchoClient(String ip , int port) throws SocketException {
        socket = new DatagramSocket();
        serverIp= ip;
        serverPort = port;
    }

创建这个对象不能手动指定端口,我们这ip和port都是对端的信息,由于UDP本身不持有对端信息,所以要在应用程序里面记录下来。

接着写下start方法

我们要从控制台读取数据作为请求,那就先

Scanner sc = new Scanner(System.in);
String request = sc.next();

然后就是把请求内容给构造成一个DatagramPacket对象 发给服务器


DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);

这里面就包含了 先把请求内容转换成字节getBytes() ,然后长度,还有目标IP和目标端口

InetAddress.getByName(serverIp) 的作用是:
把一个字符串形式的 IP 地址或主机名,解析成 Java 的 InetAddress 对象。

然后就
socket.send(requestPacket);

接着就尝试读取服务器返回的响应

 DatagramPacket responsePacket = new DatagramPacket(new byte[4069], 4069);
            socket.receive(responsePacket);

就跟服务端一样也是转换成字符串并显示出来

   String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);

最后在写一个mian方法就好了 由于在本机运行 ip地址就是127.0.0.1 端口就填服务器指定的9090

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

    }

客户端的完整代码:

package network;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = "";
    private int serverPort = 0;
    public UdpEchoClient(String ip , int port) throws SocketException {
        //创建这个对象不能手动指定端口
        socket = new DatagramSocket();
        //由于UDP本身不持有对端信息,所以要在应用程序里面把对端的情况记录下来
        serverIp= ip;
        serverPort = port;
    }
    public void start() throws IOException {

        //客户端启动
        System.out.println("客户端启动");
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台读取数据,作为请求
            System.out.println("->");
            String request = sc.next();
            //2.把请求内容构造成一个DatagramPacket对象 发给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            //3.尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4069], 4069);
            socket.receive(responsePacket);
            //4.把响应转换成字符串并显示出来
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);

        }


    }

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

    }
}

那这边就会有一个疑问 为什么那个服务器的socket一般要显式的指定一个端口号.

而客户端的socket一般不能显式指定()(不显式指定,此时系统会自动分配一个随机的端口)

其实就跟餐馆一样,餐馆就是服务端需要确定端口号,客户端就像客人,客人每次来就餐的位置可以都不一样,客户端就端口就不需要固定,直接让系统分配一个空闲即可,如果手动固定了还可能和其他程序的端口号冲突。

服务器这边手动指定端口,就不会出现冲突嘛?为啥客户端在意这个冲突,而服务器不在意呢?
服务器是在程序员手里的.一个服务器上都有哪些程序,都使用哪些端口,都是可控的!!!
程序员写代码的时候,就可以指定一个空闲的端口,给当前的服务器使用即可.

但是客户端就不可控.客户端是在用户的电脑上.
一方面,每个用户电脑上装的程序都不一样,占用的端口也不一样.

交给系统分配比较稳妥.系统能保证肯定分配一个空闲的端口

注意!

close() 不是“文件专属的方法”,它更准确的含义是:
释放这个对象占用的系统资源。
Socket 虽然不是文件,但它同样会占用操作系统资源,所以也需要 close()。

那为什么上述写的代码中,为啥没写close??

首先要知道 文件描述符,是在pcb上的.(跟随进程的)

这个socket在整个程序运行过程中都是需要使用的(不能提前关闭)当socket不需要使用的时候,意味着程序就要结束了,进程结束,此时随之文件描述符表就会销毁了(PCB都销毁了).随着销毁的过程,被系统自动回收了~~

啥时候才会出现泄露?
代码中频繁的打开文件,但是不关闭.
在一个进程的运行过程中,不断积累打开的文件,逐渐消耗掉文件描述符表里的内容
最终就消耗殆尽了.
但是如果进程的生命周期很短,打开一下没多久就关闭了.谈不上泄露.

所以看到这里又会想到 我靠这个socket到底是不是文件
GPT回答:
这里的 socket 是一个 Java 对象,它封装了底层操作系统提供的网络 Socket 资源。
Java DatagramSocket 对象

JDK 底层 native 实现

操作系统 Socket

Linux 下可能对应一个文件描述符 fd

内核 Socket 对象

所以在 Java 这一层,通常不会直接看到:

fd = 5

而是操作:

socket.receive(...)
socket.send(...)
socket.close()

之前看到 close() 也不是因为 Java 把它当文件,而是因为这个对象占用了底层系统资源,需要释放。

甚至 Java 专门有个统一接口叫:

AutoCloseable

很多完全不是文件的资源都可以 close(),比如数据库连接:

最后再补充一嘴 如果想要做一个翻译对照的话,就在这个服务端上做一个继承,写一个HashMap来做一个对照关系,再重写下process方法即可

总结下这个服务器就是

1.服务器先启动.服务器启动之后,就会进入循环,执行到receive这里并阻塞(此时还没有客户端过来呢)
2.客户端开始启动,也会先进入while循环,执行 scanner.next.并且也在这里阻塞当用户在控制台输入字符串之后,next就会返回,从而构造请求数据并发送出来~~
3.客户端发送出数据之后
服务器:就会从receive中返回,进一步的执行解析请求为字符串,执行process操作,执行send操作
客户端:继续往下执行,执行到receive,等待服务器的响应
4.客户端收到从服务器返回的数据之后,就会从receive中返回.
执行这里的打印操作,也就把响应给显示出来了.
5.服务器这边完成一次循环之后,又执行到receive这里.客户端这边完成一次循环之后,又执行到scanner.next这里.双双进入阻塞

TCP的socketAPI如何使用

TCP socket api.和 UDP的socket api 差异又很大~~但是和前面讲的文件操作,有密切联系的.

连接:通信双方是否会记录保存对端的信息.
UDP来说,每次发送数据都得手动在send方法中指定目标的地址.(UDP自身没有存储这个信息)TCP来说,则不需要.前提是需要先把连接给建立上.连接如何建立,不需要代码干预,是系统内核自动负责完成的.

对于应用程序来说
客户端这边,主要是要发起"建立连接”动作.
服务器这边,主要是要把建立好的连接从内核中拿到应用程序里~~

我们用到的两个关键的类.
1.ServerSocket(给服务器使用的类,使用这个类来绑定端口号)

2.Socket(既会给服务器用,又会给客户端用)
TCP是字节流的.传输的基本单位,是byte.

那这两个的使用简单理解就是
ServerSocket 负责“等人来连接”,Socket 负责“连接建立后真正收发数据”。

ServerSocket

负责等新的客户端

Socket

负责和已经连接的客户端通信

比如服务器要接收客户端消息:

服务器:

ServerSocket(8080)

accept()

得到 Socket

Socket.getInputStream()

读取 hello

会发现:
ServerSocket 本身不负责 getInputStream()、getOutputStream() 去通信。

开局都是一样

    private ServerSocket serverSocket=null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

start方法

public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService= Executors.newCachedThreadPool();
        while(true){
            //通过accept方法把内核中已经建立好的连接拿到应用程序
            //建立连接的细节流程都是自动完成的,应用程序只需要捡现成的
            Socket clientSocket=serverSocket.accept();
            //此处不应该直接调用processConnection 会导致服务器不能处理多个服务器
           /* Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();*/

            //线程池
            executorService.submit(()->{
                processConnection(clientSocket);
            });

        }
    } 

处理连接方法:

InputStream 和 OutputStream 就是字节流!!!就可以借助这俩对象,完成数据的"发送"和"接收”通过 InputStream进行 read操作,就是"接收"
通过OutputStream 进行 write 操作,就是“发送”

//通过这个方法处理当前连接
    private void processConnection(Socket clientSocket) {
        //进入方法,打印日志,表示当前有客户端连上了
        System.out.printf("[%s:%d] 客户端上线",clientSocket.getInetAddress(),clientSocket.getPort());
        //接下来进行数据的交互
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //使用try()方式,避免后续用完了,流对象忘记关闭。
            //由于客户端发来的数据可能是多条数据,所以我们循环处理
            while(true){
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //连接断开了,循环应该结束
                    System.out.printf("[%s:%d] 客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;

                }
                //1.读取请求并解析,此处以next作为读取请求的方式,next规则就是:读到“空白符“就返回

/*TCP是字节流通信方式.每次传输多少个字节,每次读取多少个字节都是非常灵活的.但是灵活往往是坏事~~~代码就不知道该咋办了~~往往会手动约定出,从哪里到哪里是一个完整的数据报.每次循环一次,就处理一个数据报即可。
iscanner.next 来完成请求的读取过程
上述这里就是约定了使用\n作为数据报的结束标记.就正好可以搭配*/

                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回到客户端
                //  可以把String转换为字节数组 写入到OutPutStream
                // 也可以用PrintWriter 把OutputStream 包裹下 写入字符串
                PrintWriter printWriter = new PrintWriter(outputStream);
                //此处println不是打印到控制台了,而是写入到outputStream对应的流对象中了,也就是写入到clientSocket里面
                //自然这个数据也就通过网络发送出去了
                //此处使用println带有\n也是为了后续客户端 可以使用scanner.next来读取数据
                printWriter.println(response);
                //还要刷新缓存区,保证写入
                printWriter.flush();

                //打印这次请求交互过程的内容
                System.out.printf("[%s:%d] req =%s , resp=%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);

            }

        }catch (IOException e){
            e.printStackTrace();
        } finally{
            try {
                //在这个地方,进行clientSocket的关闭
                //processConnection就是在处理一个连接。这个方法执行完毕,这个连接也就处理完了
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

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

最后补一个main方法:

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

前面写过的 DatagramSocket, ServerSocket 都没写close,但是我们说这个东西都没关系.
但是clientSocket 如果不关闭,就会真的泄露了!!!

DatagramSocket 和ServerSocket,都是在程序中,只有这么一个对象. 周期,都是贯穿整个程序的.

ClientSocket 则是在循环中,每次有一个新的客户端来建立连接,都会创建出新的clientSocket

此时,如果有很多客户端都来建立连接~~
此时,就意味着每个连接都会创建clientSocket.当连接断开
clientSocket 就失去作用了.但是如果没有手动close
此时这个socket 对象就会占据着文件描述符表的位置.

try
(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getoutputStream())
这里的关闭,只是关闭了clientSocket上自带的流对象,并没有关闭socket本身.

所以我们在最后finally里面再手动的close

客户端写法:

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 severIp,int serverPort) throws IOException {
        //需要在创建Socket同时,和服务器建立连接,此时就要告诉socket服务器在哪里
        //具体建立连接的细节,不需要手动代码干预
        //当我们new这个对象时候,操作系统内核,就开始三次握手具体细节,完成了建立连接的过程

        socket = new Socket(severIp,serverPort);

    }

    public void start(){
        //tcp客户端和udp客户端差不多

        Scanner sc=new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(outputStream);) {
            Scanner scannerNetwork=new Scanner(inputStream);
            while (true) {
                //1.从控制台读取用户输入的内容
                System.out.println("->");
                String request = sc.next();
                //2.把字符串作为请求,发给服务器
                //此处使用println是为了请求后面带上\n
                //也就是和服务器读取请求 scanner.next相呼应
                writer.println(request);
                writer.flush();

                //3.读取服务器返回的响应
                String response = scannerNetwork.next();
                //4.在界面上显示内容
                System.out.println(response);



            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

服务端写法

package network;

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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("服务器启动");
        ExecutorService executorService= Executors.newCachedThreadPool();
        while(true){
            //通过accept方法把内核中已经建立好的连接拿到应用程序
            //建立连接的细节流程都是自动完成的,应用程序只需要捡现成的
            Socket clientSocket=serverSocket.accept();
            //此处不应该直接调用processConnection 会导致服务器不能处理多个服务器
           /* Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();*/

            //线程池
            executorService.submit(()->{
                processConnection(clientSocket);
            });



        }
    }

    //通过这个方法处理当前连接
    private void processConnection(Socket clientSocket) {
        //进入方法,打印日志,表示当前有客户端连上了
        System.out.printf("[%s:%d] 客户端上线",clientSocket.getInetAddress(),clientSocket.getPort());
        //接下来进行数据的交互
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            //使用try()方式,避免后续用完了,流对象忘记关闭。
            //由于客户端发来的数据可能是多条数据,所以我们循环处理
            while(true){
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //连接断开了,循环应该结束
                    System.out.printf("[%s:%d] 客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;

                }
                //1.读取请求并解析,此处以next作为读取请求的方式,next规则就是:读到“空白符“就返回
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回到客户端
                //  可以把String转换为字节数组 写入到OutPutStream
                // 也可以用PrintWriter 把OutputStream 包裹下 写入字符串
                PrintWriter printWriter = new PrintWriter(outputStream);
                //此处println不是打印到控制台了,而是写入到outputStream对应的流对象中了,也就是写入到clientSocket里面
                //自然这个数据也就通过网络发送出去了
                //此处使用println带有\n也是为了后续客户端 可以使用scanner.next来读取数据
                printWriter.println(response);
                //还要刷新缓存区,保证写入
                printWriter.flush();

                //打印这次请求交互过程的内容
                System.out.printf("[%s:%d] req =%s , resp=%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);

            }

        }catch (IOException e){
            e.printStackTrace();
        } finally{
            try {
                //在这个地方,进行clientSocket的关闭
                //processConnection就是在处理一个连接。这个方法执行完毕,这个连接也就处理完了
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }


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

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

提一嘴回调
普通调用:
我主动调用你

回调:
我先把方法/任务交给你
以后由你主动调用我提供的方法

Thread t = new Thread(() -> {
// 这里相当于回调
});

我把一段“以后要执行的代码”交给别人,等某个时机到了,由别人反过来调用它。


匿名内部类是在“创建一个临时的类对象”,Lambda 更像是在“给函数式接口的唯一抽象方法提供一段实现代码”。
比如:

new Thread(() -> {
System.out.println("执行任务");
});

你可以理解成:

Thread需要一个 Runnable

Runnable只有一个 run()

Lambda告诉 Java:
“这个 run() 方法就执行这段代码”

这就是 Lambda 最核心的用途。

Lambda 只能用于函数式接口
函数式接口就是:
只有一个抽象方法的接口

TCP的那个代码内容经过多版优化所以不好讲出怎么一步一步得出的,所以 只能通过看代码注释理解

posted @ 2026-08-30 20:27  曾章吉的浩吧  阅读(4)  评论(0)    收藏  举报