【网络编程】从零开始彻底了解网络编程(三)

本篇博客给大家带来的是网络编程的知识点.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
❤ 欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .

王子,公主请阅🚀

要开心

要快乐

顺便进步

TCP流套接字编程

TCP 的 socket API 和 UDP 的 socket API 差异很大.
但是和的文件操作有密切联系.
TCP中关键的类:
① ServerSocket: 给服务器使用的类,使用这个类来绑定端口号.
② Socket: 既会给服务器用,又会给客户端用.
这两个类抽象了网卡这样的硬件设备.

  1. ServerSocket
    ServerSocket 是创建TCP服务端的API。
    ServerSocket 构造方法:
  1. ServerSocket 方法:

ServerSocket 方法:

如果有客户端, 和服务器建立连接,这个时候服务器的应用程序是不需要做出任何操作(也没有任何感知的),内核直接就完成了连接建立的流程(三次握手).完成流程之后,就会在内核的队列中排队(每个serverSocket 都有一个这样的队列). 应用程序要想和这个客户端进行通信,就需要通过一个 accept 方法把内核队列里已经建立好的连接对象,拿到应用程序中.

  1. Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:

Socket 方法:


前面的文章说过,TCP是字节流通信方式.
这里的InputStream 和 OutputStream 就是 字节流!!!
可以借助这俩对象,完成数据的"发送"和"接收"通过 InputStream 进行 read 操作,就是"接收".
通过 OutputStream 进行 write 操作, 就是"发送" .

  1. 利用TCP实现一个回显的程序:

服务器:

java 复制代码
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;

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

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            //1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
            Socket clientSocket = serverSocket.accept();
            //2. 处理一个连接的逻辑.
            processConnection(clientSocket);
        }
    }
    public void processConnection(Socket clientSocket) {
        //打印一个日志,表示有客户端连上了.
        System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());

        //读取请求,根据请求计算响应,最后返回响应
        //Socket 对象内部包含了两个字节流对象,把InputStream(读取),OutputStream(发送)获取到,完成后续的读写.
        try(InputStream inputStream = clientSocket.getInputStream();
                OutputStream outputStream = clientSocket.getOutputStream()) {
            //一次连接中,可能会涉及多次请求和响应
            while(true) {
                //1. 读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()) {
                    //没有下一行,说明读取完毕,客户端下线
                    System.out.printf("[%s:%d] 客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //暗含一个约定: scanner遇到空白符(\n)就返回.
                //和客户端那里相呼应.
                String request = scanner.next();
                //2. 根据请求计算响应
                String response = process(request);
                //3. 将响应发给客户端,利用PrinterWriter包裹OutputStream
                PrintWriter writer = new PrintWriter(outputStream);
                //使用PrintWriter的println方法,把响应返回给客户端
                //此处用println,而不是print就是为了结尾加上\n,与客户端对应.
                writer.println(response);
                writer.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);
        }
    }
    public String process(String request) {
        return request;
    }

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

空白符是一类特殊的字符,有 换行, 回车符,空格, 制表符,翻页符, 垂直制表符...等等。客户端发起的请求,此处会以空白符作为结束标记(此处就约定使用 \n)

客户端:

java 复制代码
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 {
        //在创建socket的同时,和服务器建立"连接"此时就得告诉Socket服务器在哪里
        //所以需要传服务器 ip 和 端口号.
        socket = new Socket(serverIp,serverPort);
    }
    public void start() {
        System.out.println("客户端上线!");
        //1. 从控制台读取用户输入的内容.
        //2. 把请求发送给服务器
        //3. 从服务器读取响应.
        //4. 把响应显示到界面上.
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while(true) {
                //1. 从控制台读取用户输入的内容.
                System.out.println("-> ");
                String request = scanner.next();

                //2. 把请求发送给服务器
                PrintWriter writer = new PrintWriter(outputStream);
                //使用println是为了让请求后面带上 换行
                //是为了和服务器读取请求 scanner.next()呼应.
                writer.println(request);
                writer.flush();

                //3.  读取服务器返回的响应.
                Scanner scannerNetwork = new Scanner(inputStream);
                String response = scannerNetwork.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();
    }
}

上述代码存在两个问题:

  1. 服务器程序会出现文件资源泄露的情况.
    前面写过的DatagramSocket, ServerSocket 都没写 close,但没关系,因为DatagramSocket 和 ServerSocket, 都是在程序中,只有这么一个对象,生命周期贯穿整个程序的. 但 clientSocket则是在循环中,每次有一个新的客户端来建立连接,都会创建出新的 clientSocket .
    如果有很多客户端都来建立连接,此时,就意味着每个连接都会创建 clientSocket. 如果没有手动 close此时这个 socket 对象会占据着文件描述符表的位置,一旦占满,文件资源就泄露了.
    那要怎么办呢?


在 processConnection方法末尾加上下面的代码即可.

java 复制代码
 finally {
            //手动关闭clientSocket,避免文件资源泄露.
            try {
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

既然文件资源泄露是因为clientSocket没关,那能不能在processConnection(clientSocket);执行完之后,再把clientSocket关闭呢?

java 复制代码
public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            //1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
            Socket clientSocket = serverSocket.accept();
            //2. 处理一个连接的逻辑.
            processConnection(clientSocket);
            clientSocket.close();
        }
    }

显然是不可以的,如果 在processConnection方法执行过程中遇到问题,抛出异常.那么还没执行到clientSocket.close();循环就结束了.
考虑到抛异常的问题, 于是就将代码改成try with source结构:

java 复制代码
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            //1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
            try(Socket clientSocket = serverSocket.accept()) {
                //2. 处理一个连接的逻辑.
                processConnection(clientSocket);
            }
        }
    }
  1. 多个客户端无法同时与服务器进行交互.

先设置允许存在多个客户端.

同时运行两个客户端,在服务端上只能看到第一个客户端. 第二个客户端与服务器没有进行任何交互.


模拟两个客户端连接服务器的过程:
① 第一个客户端过来之后accept 就返回了,得到一个clientSocket.进入了 processConnection. 又进入了一个 while 循环, 这个循环中,就需要反复处理客户端发来的请求数据.如果客户端没发请求,服务器的代码
就会阻塞scanner.hasNext处.
② 此时此刻,第二个客户端也过来建立连接了, 连接是可以成功建立的. 建立成功之后,连接对象就会在内核的队列里等待代码通过 accept. 但是第二个客户端永远不可能执行accept方法,除非第一客户端断开连接.
怎么让两个客户端同时执行呢?

关键就是,让两个循环能够"并发"执行. 这就涉及到前面学过的多线程知识了.
创建新的线程,让新的线程去调用 processConnection
主线程就可以继续执行下一次 accept 了,新线程内部负责 processConnection的循环此时意味着,每次有一个客户端,就都得给分配一个新的线程, 又因为涉及到频繁创建销毁线程. 所以更好的方式是用线程池, 不用当然也可以, 用的话效率高一些.

解法Ⅰ 服务器引入多线程:

java 复制代码
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
                //1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
                Socket clientSocket = serverSocket.accept();
                Thread t = new Thread(() -> {
                    //2. 处理一个连接的逻辑.
                    processConnection(clientSocket);
                });
                t.start();
        }
    }

解法Ⅱ 避免频繁创建销毁线程, 服务器引入线程池:

java 复制代码
    //服务器引入线程池
    public void start() throws IOException {
        System.out.println("服务器");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true) {
            //1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
            Socket clientSocket = serverSocket.accept();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

本篇博客到这里就结束啦, 感谢观看 ❤❤❤
🐎期待与你的下一次相遇😊😊😊

相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理6 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
DianSan_ERP6 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
feifeigo1236 天前
matlab画图工具
开发语言·matlab