当前位置: 首页 > article >正文

Java 网络编程 ②-TCP Socket

这里是Themberfue

        在上一节中,我们简单认识了 TCP协议 和 UDP协议 以及 基于UDP Socket 编写了简单的网络通信代码

        本节我们将基于 TCP Socket 编写简单的网络通信代码


TCP Socket 

        类似 UDP Socket,Java也基于 TCP协议 进行了一些接口的封装

        基于 TCP 封装的 SocketAPI:

        ServerSocket:ServerSocket 是创建TCP服务端Socket的API。

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

        ServerSocket提供的方法

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

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

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

        Socket提供的方法

方法签名方法说明
InetAddressgetInetAddress()返回套接字所连接的地址
InputStreamgetInputStream()返回此套接字的输入流
OutputStreamgetOutputStream()返回此套接字的输出流

代码编写与讲解 

TcpEchoServer

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

/**
 * @author: Themberfue
 * @date: 2024/11/14 19:24
 * @description:
 */
public class TcpEchoServer {
    // TCP 协议专门用于创建服务端的对象
    private ServerSocket socket = null;

    // 这里和 UDP 服务器类似,也是在构造对象时,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        this.socket = new ServerSocket(port);
    }

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

        // 这种情况一般不会用 fixedThreadPool,不然处理客户端请求的数量就固定了
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 需要一直执行里面的逻辑
        // 一直保持去连接客户端的状态
        while (true) {
            // 对于 TCP 来说,需要先处理和客户端发来的连接(有连接)
            // 通过读写 clientSocket,与客户端进行通信
            // 如果没有客户端发起连接,那么就阻塞在 accept
            // 这里的阻塞和多线程的阻塞有本质区别

            // 如果只是一个线程处理连接和读取请求两个工作
            // 该线程会在 hasNext() 那里阻塞,就不能处理下一个客户端的连接了
            // 所以引入多线程来处理,这里的主线程处理与客户端的连接
            // 连接到一个客户端后,就创建一个线程来处理客户端发送来的请求
            Socket clientSocket = socket.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());

        // TCP 是通过面向字节流的
        try (OutputStream outputStream = clientSocket.getOutputStream();
             InputStream inputStream = clientSocket.getInputStream()) {
            // 针对 OutputStream 套了一层
            PrintWriter printWriter = new PrintWriter(outputStream);

            // 针对 InputStream 套了一层
            Scanner scanner = new Scanner(inputStream);

            // 这里也是反复执行,一个客户端可能发来多次请求
            while (true) {
                // 若该客户端关闭后,表示不会发送请求,自然就读不到请求了,此时,该客户端下线
                // 若一直没有读到请求,就在这里阻塞,直到客户端那边发来请求
                if (!scanner.hasNext()) {
                    // 断开连接了
                    System.out.printf("[%s:%d]:客户端下线\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 1. 读取请求并解析
                String request = scanner.next();

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

                // 将计算后的响应发送给客户端
                printWriter.println(response);
                // flush 起到刷新缓冲区的作用
                printWriter.flush();

                // 打印日志
                System.out.printf("[%s:%d] req:%s res:%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 tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

        与 UDP 不同的是,TCP协议是 有连接,所以在进行网络通信前,需要客户端和服务器端进行连接,交换彼此的一些信息。

        服务器端的 ServerSocket 通过 accept 方法尝试与客户端 Socket 连接;如果连接成功,则返回一个 Socket对象;没连接到的话,就一直阻塞在这里,直到连接到客户端。

         与 UDP 又不同的是,TCP 是以字节为单位进行读写数据的,故使用 InputStream 和 OutputStream 进行读写数据。

        由于客户端可能不止发送一个请求,所以也是使用死循环,反复尝试读取数据。

        若一直没有读到请求,则会在 hasNext() 这里阻塞,直到客户端发送请求,成功读取到数据。

        写入数据时,必须使用 println,因为这里隐含的表示以 \n 为结束符号结束写入,若没有这一符号,则程序不知道什么时候停止写入。

        在写入时,并不是直接写入到网卡的,而是写入到缓冲区,故在写完时,还需刷新缓冲区,确保数据成功写入到网卡上。

        在客户端断开连接后,需要关闭为客户端创建的 Socket 连接

        对于 TCP协议 来说,需要先处理和客户端发来的连接,通过读写 clientSocket,与客户端进行通信,如果没有客户端发起连接,那么就会阻塞在 accept,这里的阻塞和多线程的阻塞又本质区别

        如果只是一个线程处理连接和读取请求两个工作,由于服务端线程会在 hasNext() 那里阻塞,就不能处理下一个客户端的连接了,所以引入多线程来处理,这里的主线程处理与客户端的连接,连接到一个客户端后,就创建一个线程来处理客户端发送来的请求。

        但是频繁的创建和销毁线程,开销很大,所以引入线程池来处理客户端的请求


TcpEchoClient

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

/**
 * @author: Themberfue
 * @date: 2024/11/14 19:37
 * @description:
 */
public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int port) throws IOException {
        // TCP 就不需要单独创建额外的变量来保存服务器的ip和端口了
        // 通过连接会自动保存对端的信息

        // 直接把字符串的IP的值,设置过来
        this.socket = new Socket(serverIp, port);
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        try (OutputStream outputStream = socket.getOutputStream();
             InputStream inputStream = socket.getInputStream()) {
            // 同样为了使用方便,套壳使用
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            // 客户端可能发送的请求不止一次,所以通过死循环的逻辑(简单的从控制台读取,所以这样)
            // 该客户端进程结束时,请求就不发了,自然服务端那边就不能读取到了
            // 这边这个客户端连接就与服务端断开了
            while (true) {
                // 从控制台读取客户端的请求
                String request = scanner.next();
                // 将客户端的请求发送到服务端
                // 这个操作只是将数据写入到 "缓冲区",并没有真正写入网卡中
                printWriter.println(request);
                // 这里清除缓冲区后,才真正将数据写入到网卡中
                printWriter.flush();

                // 读取服务端发送来的请求
                String response = scannerNet.next();
                System.out.printf("req:%s res:%s\n", request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

网络编程的知识到这里就差不多结束了~~~

下节我们将进入跟多姿多彩的网络世界里学习~~~ 

毕竟不知后事如何,且听下回分解 

❤️❤️❤️❤️❤️❤️❤️

 


http://www.kler.cn/a/450502.html

相关文章:

  • 【Linux探索学习】第二十三弹——理解文件系统:认识硬件、探索文件在硬件上的存储问题
  • js事件机制详解
  • 一体式IO模块:打印机加工产线国产化降本增效的新利器
  • 环网冗余CAN转光纤 CAN光端机在风电项目应用
  • 前端开放性技术面试—面试题
  • SpringBoot相关漏洞学习资料
  • Electron -- Electron应用主要核心(二)
  • ABAP开发-权限控制
  • Android 蓝牙开发-传输数据
  • AIDD - 探索语言模型在药物分子生成方面的应用
  • iptables交叉编译(Hisiav300平台)
  • [cisco 模拟器] ftp服务器配置
  • 一个简单封装的的nodejs缓存对象
  • 828考研资料汇总
  • C++ QT chip layout tool开发浅思
  • 【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
  • PTA数据结构题目:链表操作集合
  • 近实时”(NRT)搜索、倒排索引
  • Unity3d 基于UGUI和VideoPlayer 实现一个多功能视频播放器功能(含源码)
  • GitLab 停止为中国区用户提供 GitLab.com 账号服务
  • kong网关使用pre-function插件,改写接口的返回数据
  • 隧道可视化技术开拓智能建设新航道
  • 基于Spring Boot的摄影器材租赁回收系统
  • 神经网络图像隐写术:用AI隐藏信息的艺术
  • 1小时放弃Rust(1): Hello-World
  • *【每日一题 基础题】 [蓝桥杯 2024 省 B] 好数