java——网络编程套接字

在这里插入图片描述

T04BF

👋专栏: 算法|JAVA|MySQL|C语言

🫵 今天你敲代码了吗

目录

    • 2.网络编程套接字
      • 2.1 socket api
      • 2.2 TCP和UDP之间的区别
        • 有连接 vs 无连接
        • 可靠传输 vs 不可靠传输
        • 面向字节流vs面向数据报
        • 全双工 vs 半双工
      • 2.3UDP数据报套接字编程
        • UDP 回显服务器
        • UDP客户端
      • 2.4 TCP流套接字编程
        • TCP服务器
        • TCP客户端

2.网络编程套接字

2.1 socket api

操作系统实际上为我们提供了进行网络编程的API,称为"socket api"
而在操作系统中,提供的socket api不止一套,有很多套
(1)流式套接字 => 给TCP使用的
(2)数据报套接字 => 给UDP使用的
(3)Unix域套接字 => 不能跨主机通信 ,只是本地主机进程与进程之间的通信(现在很少使用了)

2.2 TCP和UDP之间的区别

TCP和UDP都是传输层协议
但是由于二者之间的差异太大了,因此需要两套 API分别来表示
具体的特点就是
TCP是:有连接,可靠传输,面向字节流,全双工
UDP是:无连接,不可靠传输,面向数据报,全双工

有连接 vs 无连接

有连接就好比于 打电话的过程中,拨通电话之后,需要对方接受才能对话
无连接就好比于 发微信,不需要先接通,直接就可以发

可靠传输 vs 不可靠传输

首先要区分 可靠性 != 安全性
安全指的是,你传输的数据是否容易被黑客截取到,是否会造成严重后果
而可靠性,指的是,你传输的数据,尽可能的到达对方
注意:是尽可能,不是完全,只是尽力做到,不能保证100%

在当前网络通信过程中,会出现很多的意外情况,比如丢包就是很常见的情况
就比如A给B传输10个数据包,但是实际上B只是收到9个,这就是丢包

为什么会出现丢包
本质上就是因为当前网络环境太复杂了
A 给 B 传输数据,中间可能会经过很多的路由器和交换机,而这些路由器和交换机不只是转发你的数据,还要转发很多数据
当某个路由器 / 交换机非常繁忙的时候,要处理的数据量,已经超出了当前设备硬件的水平极限,就无法转发,某些数据会被直接丢弃掉

而且,丢包是随机的过程,我们无法预知,啥时候会出现丢包,也无法预知,哪个交换机 / 路由器会出现丢包

为了对抗丢包,就引入了"可靠传输",TCP就实现了可靠传输特点,内部就提供了一系列的机制来实现可靠传输

即使如此,TCP还是只能尽可能,无法保证数据100%到达对端,因为在极端情况下,可能会出现网线断开的情况

相比之下,UDP是不可靠传输,传输数据的时候,压根不关心对方是否收到 那UDP还有什么用??

实际上,可靠传输是有代价的

最典型的就是,效率会大打折扣,即UDP 比 TCP快

面向字节流vs面向数据报

我们之前说过.文件操作就是字节流的,即读写操作非常灵活

TCP和文件操作,具有相同的特点

而面向数据报就不一样了,此时传输数据的单位,是一个个的UDP数据报,一次读写,只能读写一个完整的UDP数据报,不能搞半个,也不能搞一个半

全双工 vs 半双工

全双工指的是,一条链路,能够进行双向通信

半双工指的是,一条链路,只能进行单向通信

而TCP和UDP都是全双工

2.3UDP数据报套接字编程

UDP的socket api主要就是两个类:

(1)DatagramSocket

系统中本来就有socket这个概念,DatagramSocket就是对于操作系统中socket的封装

在系统中,socket可以理解成是一种"文件",socket文件就可以理解成操作"网卡"这种硬件设备的抽象表示形式,而我们针对socket文件的读写操作就相当于是针对网卡这个硬件设备进行读写
其实本质上就类似于我们的普通文件,就是针对硬盘这样的硬件设备进行操作
此处,DatagramSocket就可以是视为操作网卡的"遥控器",针对这个对象进行读写操作,就是在针对网卡进行读写操作

DatagrmeSocket的构造方法:
在这里插入图片描述
DatagramStoket的方法

在这里插入图片描述
注意:socket也是一种文件,用完了要关闭.否则会占着文件描述符表的一个表项

(2)DatagramPacket

我们可以发现,在上面DatagramSoket的方法参数中,传的就是 DatagramPacket类型

DatagramPacket就是 针对 UDP数据报的一个抽象表示,

也就是说,一个DatagramPacket对象,就相当于一个Udp数据报

一次发送 / 一次接受,就是传输了一个DatagramPacket对象

构造方法:
在这里插入图片描述
方法
在这里插入图片描述

UDP 回显服务器
public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
}

接下来就要通过构造方法指定服务器的端口号

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        //指定端口号
        this.socket = new DatagramSocket(port);
    }
}

这里的端口号是为客户端提供的

客户端是主动的一方,服务器是被动的一方

客户端要知道服务器在哪里,才能进行主动通信

还要确保,一个端口号不能同时被两个或 多个进程来关联,即不能重复

在实际开发,端口号指定是程序员自行指定的,但是你要确保 端口号是 合法的 (1 - 65535)
并且要保证,不能和别的进程使用的端口号重复(实际上重复是个小概率事件,实验一下即可看看是否重复)

接下来还要让 服务 能够不停的处理请求,不停的返回响应

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        //指定端口号
        this.socket = new DatagramSocket(port);
    }
    
    public void start () {
        System.out.println("服务器启动!!");
        while (true) {
            //1) 读取请求并解析
            //2)根据请求计算响应
            //3)把响应写回到客户端   
        }
    }
}

接着就要接受客户端的请求,就要准备一个DatagramPacket来装请求

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            //读取数据并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
    }
}

进行receive操作,就是准备一个空盘子,把空盘子交给 它进行装数据

此时这个resultPacket对象就是一个UDP数据报,就包含两部分

一部分是报头(通过类的属性来表示的)

一部分是载荷(通过构造方法传递的字节数组,作为持有载荷的空间,且这个载荷的空间是我们自己new好的字节数组交给他的)

之后我们就可以针对请求,计算出响应

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            //读取数据并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //为了在java代码中方便处理,可以将数据报里面的二进制数据拿出来,构造成字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());//String的构造方法,通过字节数组来构造字符串

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

    private String process(String request) {
        //当前是回显服务器
        return request;
    }
}

接下来将响应写回到客户端

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            //读取数据并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //为了在java代码中方便处理,可以将数据报里面的二进制数据拿出来,构造成字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());//String的构造方法,通过字节数组来构造字符串


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

            //把响应写回给客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req=%s resp=%s\n",requestPacket.getAddress(),requestPacket.getPort(),request,response);
        }
    }

    private String process(String request) {
        //当前是回显服务器
        return request;
    }
}

注意,接受请求的时候,只需要一个空对象即可

但是,发送则是要有数据的对象,不光要有数据,还要指定数据发给谁

但是谁给我发的请求,UDP socket本身没有记录,是在DatagramPacke这个对象里面有记录
在这里插入图片描述
这个对象里面就包含了 发送方的IP和端口号,直接拿过来放到packet对象即可

此时我们的服务器逻辑就准备好了

直接启动即可接受到客户端的请求(注意在main方法里面指定服务器的端口号)

public class UdpEchoServer {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            //读取数据并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //为了在java代码中方便处理,可以将数据报里面的二进制数据拿出来,构造成字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());//String的构造方法,通过字节数组来构造字符串


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

            //把响应写回给客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            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(4090);
        server.start();
    }
}

写到这里,还有几个细节问题:

(1)如果没有客户端发送请求,服务器的代码就会在receive这里阻塞

这里的阻塞是等待IO,是由系统内核来控制的,直到由客户端发送来请求为止

(2)一般的这种服务器程序都是 这种死循环 ,如果要结束就可以直接强制结束

UDP客户端
public class UdpEchoClient {
    //操作网卡
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket();
    }
}

但是此时我们还要知道 服务器的IP 和 端口号,才能发送请求

客户端是需要通过额外的途径知道 服务器和 IP和端口号是啥的

因此,服务器的Ip和端口号得是固定的,不能老变

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 static void main(String[] agrs) throws SocketException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",4090);
    }
}

此时的Ip"127.0.0.1",指的是 服务器和客户端在一个主机上,就固定写这个Ip地址(指的是环回Ip,系统提供的特殊IP)

如果是 在不同主机上,那么服务器Ip是啥就写啥

实际上客户端也是需要端口号 和 ip的

在一次通信的过程中,至少需要知道4个核心指标

(1)源Ip

(2)源端口

(3)目的端口

(4)目的Ip

对于客户端来说,我们刚刚指定的 服务器ip和 端口号 是 目的ip和端口号

源Ip也就是客户端所在的Ip(在我们这个程序可以视为 是 127.0.0.1)

源端口 ,在构造socket对象的时候,没有指定端口号 ,没指定不代表没有

实际上操作系统自动分配了一个空闲的(不和其他程序冲突的)端口号过来了

这个自动分配的端口号 ,每次重新启动程序都可能不一样

而客户端 和 服务器的代码逻辑实际上是对应的

客户端方面,我们要实现

(1)从控制台读取用户输入

(2)构造请求并发送

(3)从服务器读取响应

(4)把响应打印到控制台上

    public void start() throws IOException {
        System.out.println("客户端启动!!");
        while(true) {
            //从控制台读取字符串
            Scanner scanner = new Scanner(System.in);
            String request = scanner.next();
            //构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length);
            socket.send(requestPacket);
            //从服务器读取到响应
           //将响应打印到控制台
        }
    }
    public void start() throws IOException {
        System.out.println("客户端启动!!");
        while(true) {
            //从控制台读取字符串
            Scanner scanner = new Scanner(System.in);
            String request = scanner.next();
            //构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length);
            socket.send(requestPacket);
            //从服务器读取到响应
           //将响应打印到控制台
        }
    }

但是客户端要知道,当前这个数据要传给谁,因此在requestPacket里面要带有服务器的Ip和端口号

    public void start() throws IOException {
        System.out.println("客户端启动!!");
        while(true) {
            //从控制台读取字符串
            Scanner scanner = new Scanner(System.in);
            String request = scanner.next();
            //构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            //从服务器读取到响应
           //将响应打印到控制台
        }
    }

接下来就是读取响应

    public void start() throws IOException {
        System.out.println("客户端启动!!");
        while(true) {
            //从控制台读取字符串
            Scanner scanner = new Scanner(System.in);
            String request = scanner.next();
            //构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),0,request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
             //从服务器读取到响应
             DatagramPacket responsePacket = new DatagramPacket(new byte[4090],4090);
             socket.receive(responsePacket);
           //将响应打印到控制台
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }
    }
}

此时服务器和客户端就能建立连接了
在这里插入图片描述
在这里插入图片描述

怎么在idea上运行多个客户端呢??
在这里插入图片描述
在这里插入图片描述

但是此时如果是不在同一个主机上,也是只有在同一个局域网才能进行通信

要想在广域网上使用,必须要有 能够在广域网使用的 “公网Ip”

我们自己要拥有公网Ip,只能通过买"云服务器"




有一个问题就是

似乎在我们上面Udp数据报套接字的例子里面,没有用到close??

实际上无论是在客户端还是服务器

都会有:

在这里插入图片描述

而这个对象的生命周期和整个程序的生命周期是一样的,只要程序还在运行着,那么这个socket对象就不能提前释放

而当我们手动去关闭这个程序的进程的时候,意味着进程里面的所有资源都会释放掉,包括持有的内存和文件描述符表,此时也不需要我们额外使用 close去关闭

如果在某些程序里,Socket对象的什么周期与 进程的生命周期不一样,那么就需要调用close

2.4 TCP流套接字编程

在TCP中,socket api也是两个关键的类

(1)ServerSocket

是专门给服务器用的

构造方法:
在这里插入图片描述
方法:
在这里插入图片描述
(2)Socket

这个类是客户端和服务器都要使用的类

构造方法:
在这里插入图片描述
此时调用构造方法本身,就能够和指定的主机建立连接

方法:
在这里插入图片描述
实际上ServeSocket 和 Socket 这两个东西起到的作用是截然不同的

TCP服务器
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();//接听
        }
    }

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

此时程序一启动就会立马执行到accept这里,如果没有客户端连接过来,那就阻塞等待,直到有客户端真的连接上来了

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();//接听
            processConnect(clientSocket);
        }
    }

    private void processConnect(Socket clientSocket) {
        //通过这一个方法来处理一个连接
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
    }

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

接下来我们要从socket中获取到流对象,进一步进行操作

    private void processConnect(Socket clientSocket) {
        //通过这一个方法来处理一个连接
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //从socket获取到流对象,进一步进行操作
        try(InputStream inputStream = clientSocket.getInputStream();
        OutputStream outputStream = clientSocket.getOutputStream()){
            while(true) {
                //针对某一个客户端,处理多次请求
                
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

此时读取客户端请求的方法是:

String request = scanner.next();

此时如果我们按照inputStream.read的方法来读,那么读到的数据还要进行转化

但是如果直接用Scanner来读,直接读出来的就是String(Scanner已经帮我们进行了上述的转化)

而对于next()方法,就会读取带空白符才算 读取完毕

空白符是一类字符的统称,包括但是不限于换行,回车,制表符,翻页符…
因此为了服务器更精确的处理一次请求,客户端那边子传输数据的时候,就要每一个请求的末尾添上空白符,比如填写 \n

这是我们进行的一个"约定"

由于TCP是按照字节来传输的,而实际上,我们更希望的是,若干个字节能够构成一个"应用层数据报"
如何区分多个应用层数据报,就是通过"分隔符"的方式;来约定
上述我们就是在约定说,使用空白符作为一次请求的一个结束标记
这个方式是可以随心所欲的,只要是客户端和服务器一致的行为即可

如果发现后续没有数据了,就说明客户端断开了

    private void processConnect(Socket clientSocket) throws IOException {
        //通过这一个方法来处理一个连接
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
        OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                //针对一个客户端,处理多次请求
                //(1)读取请求并解析
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

而这里的Scanner是带有阻塞功能的,就会阻塞等待请求的到来

这里的等待请求到来 和 客户端断开 是可以明确区分开来的.是不一样的

Scanner可以认为是关联到服务器这边的socket文件的

而socket是可以感知到tcp连接断开的(本身是系统内核的一系列操作,操作系统知道tcp连接断开后,就会通知socket)

此时对应的Scanner读取到的文件,就下单于是"无效文件",类似于读取到"EOF"这样的效果

    private void processConnect(Socket clientSocket) {
        //通过这一个方法来处理一个连接
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //从socket获取到流对象,进一步进行操作
        try(InputStream inputStream = clientSocket.getInputStream();
        OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            while(true) {
                //针对某一个客户端,处理多次请求
                //(1)读取请求并解析
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                }
                String request = scanner.next();
                //(2)根据请求计算相应
                String response = process(request);
                //(3)把响应写回给客户端
                outputStream.write(response.getBytes());
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    private String process(String request) {
        //回显服务器

        //服务器这边也是要加上一个换行符,以便于 客户端在读取响应的时候,有明确的分割符
        return request + "\n";
    }

那么此时服务器这边就写好了

但是此时上述代码还存在很严重的bug

1.服务器这边创建的Socket是没有进行关闭操作的
在这里插入图片描述
这个对象在processConnect使用之后,没有进行close,是不科学的

与上面的ServerSocket对象不一样的是,ServerSocket对象的生命周期是伴随着整个服务器的进程的

但是clientSocket就不行了,服务器会对应多个客户端,每个客户端连接之后都会对应一个clientSocket

如果用完了不关闭,就会使得当前clientSocket的文件描述符得不到释放,引起了文件资源的泄露

我们在processConnect使用完毕后,执行关闭逻辑即可


    private void processConnect(Socket clientSocket) throws IOException {
        //通过这一个方法来处理一个连接
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
        OutputStream outputStream = clientSocket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            while (true) {
                //针对一个客户端,处理多次请求
                //(1)读取请求并解析
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                //(2)根据请求计算响应
                String response = process(request);
                outputStream.write(response.getBytes());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            clientSocket.close();
        }
    }

2,此时的服务器是无法同时给多个客户端提供服务的

本质上就是,第一个客户端连接上服务器后,此时accept方法就返回了,但是此时代码进入到processConnect方法中就循环起来了,此时如果第二个客户端想要连接上服务器,服务器的accept方法是执行不到的

我们可以引入多线程来处理多个客户端

在这里插入图片描述
或者我们直接使用线程池

在这里插入图片描述
但是实际上上述方法存在一个严重的问题:

在以前 ,互联网规模不大的时候,一个服务器在同一个时刻也处理不了几个客户端,那么上述直接使用多线程的方法就无所谓

但是随着时代的发展,现如今一个服务同一时刻要处理的客户端就有很多了

一个服务器在同时创建出几百个线程压力就已经很大了

更何况现如今可能要同时搞上万个线程

这里真正的核心解方案是 IO多路复用 + 多个服务器(分布式系统)

所谓IO多路复用就是指,使用一个线程就管理多个socket,而这些socket往往不是 同时有数据要处理,而是同一时刻,只有少数数据需要读取数据

而IO多路复用是系统内核已经处理好了,直接提供API给我们调用,在java中提供了NIO这样的一组类,对上述操作进行了进一步的封装

在实际开发中又有一些封装层次更高的框架处理上述逻辑,这次成熟的框架背后也是直接使用的NIO,因此我们通常不会直接使用NIO

TCP客户端

对于客户端那边,原理也是类似

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //一旦调用这样的构造方法,就会和服务器之间建立联系
        //具体的连接步骤都是系统内核完成的
        this.socket = new Socket(serverIp,serverPort);
    }

    public void start() {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerNetwork = new Scanner(inputStream);
            //从控制太读取数据
            while(true) {
                System.out.print("输入你要发送的数据: ");
                String request = scanner.next() + "\n";
                //把请求发送给服务器
                outputStream.write(request.getBytes());

                //从服务器得到响应
                if(!scannerNetwork.hasNext()) {
                    break;
                }
                String response = scannerNetwork.next();
                //将响应输出到控制台
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


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

此时就可以进行交互了

感谢您的访问!!期待您的关注!!!

在这里插入图片描述

T04BF

🫵 今天记得敲代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/664572.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mariadb操作命令指南

MariaDB简介 ​ 以下内容仅是站长或网友个人学习笔记、总结和研究收藏。不保证正确性,因使用而带来的风险与本站无关! 数据库应用程序与主应用程序分开存在,并存储数据集合。 每个数据库都使用一个或多个API来创建,访问&#xf…

ch4网络层---计算机网络期末复习(持续更新中)

网络层概述 将分组从发送方主机传送到接收方主机 发送方将运输层数据段封装成分组 接收方将分组解封装后将数据段递交给运输层网络层协议存在于每台主机和路由器上 路由器检查所有经过它的IP分组的分组头 注意路由器只有3层(网络层、链路层、物理层) 网络层提供的服务 一…

ArcGIS教程(02):创建多模式网络数据集

启动“新建网络数据集”向导 命名网络并选择源要素类 输入网络数据集名称【ParisMultimodal_ND】,点击【下一页】 点击【全选】网络数据集中的要素类 点击【下一页】 设置连通性和高程策略 点击【连通性】 Metro_Entrances 的每个要素与街道要素类的折点重…

企业文件加密:保障知识产权与客户隐私

在数字化时代,企业文件的安全成为了保护知识产权和客户隐私的关键。随着网络攻击和数据泄露事件的日益增多,企业必须采取强有力的措施来确保其敏感信息的安全。文件加密技术作为一项重要的数据保护手段,对于维护企业的竞争力和客户信任至关重…

第八十三节 Java面向对象设计 - Java方法重载

Java面向对象设计 - Java方法重载 在同一类中具有多个具有相同名称的方法称为方法重载。 类中具有相同名称的方法可以是声明的方法,继承的方法或两者的组合。 重载方法必须具有不同数量的参数,不同类型的参数或两者。 方法的返回类型,访问…

数字孪生技术为何备受各行业青睐?

数字孪生技术近年来在各行业中受到越来越多的重视,这是因为它具备了显著的优势和广泛的应用前景。数字孪生是指利用数字化技术,在虚拟空间中创建一个与现实世界对应的虚拟模型,通过数据的实时交互和反馈,实现对物理实体的模拟和监…

3d模型移动中心点偏移太远怎么解决?---模大狮模型网

在3D建模和动画制作中,移动模型时确保中心点的准确性至关重要。然而,有时候在移动模型时,中心点可能会偏移得太远,导致操作不便甚至影响到后续的工作流程。本文将介绍在3D模型移动中心点偏移太远时的常见原因,并提供解…

Linux系统编程——动静态库

目录 一,关于动静态库 1.1 什么是库? 1.2 认识动静态库 1.3 动静态库特征 二,静态库 2.1 制作静态库 2.2 使用静态库 三,动态库 3.1 制作动态库 3.2 使用动态库一些问题 3.3 正确使用动态库三种方法 3.3.1 方法一&…

sprintboot中拦截器的使用

文章目录 1. 为什么要使用拦截器2.拦截器的注册3.创建一个登录拦截器 1. 为什么要使用拦截器 1.权限检查:进入程序判断是否登录,没有登录,直接返回跳转到登录界面 2.性能监控:通过拦截器在进入处理程序之前记录开始时间&#xff…

java家政上门系统源码,一套同城预约、上门服务的家政系统源码

一款同城预约、上门服务的家政系统源码,用户端、服务端、管理端各端相互依赖又相互独立,支持选择项目、选择服务人员的下单方式,支持多城市并且设置每个城市专属服务项目。 技术架构:java1.8springboot mysql htmlThymeleaf uni…

【AI+知识库问答】沉浸式体验了解 AI知识库问答fastGPT

之前写过一篇文章 【AI本地知识库】个人整理的几种常见本地知识库技术方案 , 由于当时主要是针对AI本地知识库, 所以没列fastGPT。 最近经常刷到fastGPT,这里单独水一篇。 FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,…

UML静态图-对象图

概述 静态图包含类图、对象图和包图的主要目的是在系统详细设计阶段,帮助系统设计人员以一种可视化的方式来理解系统的内部结构和代码结构,包括类的细节、类的属性和操作、类的依赖关系和调用关系、类的包和包的依赖关系。 对象图与类图之间的关系&…

centos7 openssh9.7p 制作rpm包

centos7 openssh9.7p 制作rpm包 下载源码包:通过git开源打包源码准备编译打包环境编译打包上传rpm包到需要更新的服务器,并更新 下载源码包: 一般只用ssh源码就可以了 cd /root wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.7p…

UI线程和工作线程

引用:windows程序员面试指南 工作线程 只处理逻辑的线程,例如:启动一个线程,用来做一个复杂的计算,计算完成之后,此线程就自动退出,这种线程称为工作线程 UI线程 Windows应用程序一般由窗口…

ST - 如何利用SCR轻松驱动AC/DC转换器启动?

过去十年,新装服务器的市场需求增长迅猛,2015到2022年复合年均增长率达到了11%。拉动市场增长的动力主要来自以下几个方面:首先,个人文件无纸化和企业办公数字化进程加快;其次,健康危机期间的居家办公&…

【LeetCode算法】第100题:相同的树

目录 一、题目描述 二、初次解答 三、官方解法 四、总结 一、题目描述 二、初次解答 1. 思路:二叉树的先序遍历。采用递归的先序遍历方法,首先访问根节点若不同则返回false,其次访问左子树和右子树。在访问左右子树时,需要注意…

大模型算法办备案全网最详细说明(+附件)

已成功备案产品(近130家,不包括审核中的) 一、大模型算法备案的强制性 二、生成式人工智能(大语言模型)安全评估要点 三、大模型备案必备材料重点说明 四、大模型备案填报流程 五、大模型备案时间成本对比 六、备案建议 附录、过程性材料 一…

977. 有序数组的平方 - 力扣

1. 题目 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 2. 示例 3. 分析 我们当然可以遍历数组平方元素,然后再使用sort排序,但这里时间复杂度就为 O(logN) 了。 我…

uniapp的tooltip功能放到表单laber

在uniapp中,tooltip功能通常是通过view组件的hover-class属性来实现的,而不是直接放在form的label上。hover-class属性可以定义当元素处于hover状态时的样式类,通过这个属性,可以实现一个类似tooltip的效果。 以下是一个简单的例…

【Python】解决Python报错:AttributeError: ‘instance‘ object has no attribute ‘xxx‘

🧑 博主简介:阿里巴巴嵌入式技术专家,深耕嵌入式人工智能领域,具备多年的嵌入式硬件产品研发管理经验。 📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向…