TCP流套接字编程

2023-12-13 04:26:07

TCP流套接字编程

TCP和UDP差距是很大的,在数据传输方面,UDP是面向数据报的,而TCP是面向字节流的的,下面列出了使用TCP来实现网络编程所依赖的Socket类,通过这些类和具体的例子,来详细的讲解TCP网络编程。

ServerSocket API

ServerSocket 是Java提供给我们创建TCP服务端Socket所使用的API。

ServerSocket 的构造方法如下:

构造方法方法说明
ServerSocket(int port)创建一个服务端的Socket对象,并绑定到指定的端口

ServerSocket 方法:

方法方法说明
Socket accept()在客户端连接后,通过accept()方法,可以拿出内核中已经于客户端建立好的连接对象,这个对象就是Socket对象,如果没有建立好的连接,则阻塞等待
void close()关闭此套接字

Socket API

在Java中,也给我们提供了一个Socket类,而这个 Socket 既会给客户端使用又会给服务器使用,以上这个 Server Socket 和 Socket 这两个类都是用来表示系统中的Socket文件的,也就是抽象了网卡这样的概念。

Socket 构造方法:

构造方法方法说明
Socket(String host, int port)创建一个客户端的Socket,并指定对端主机的IP,对端进程的端口号,然后建立连接

Socket 方法:

方法方法说明
InetAddress getInetAddress()返回Socket所连接的地址
InpuStream getInputStream()返回Socket的输入流
OutputStream getOutpubStream()返回此Socket的输出流

注意:UDP是无连接的,而TCP是有连接的

这里的“有连接”和“无连接” 不是传统意义上的连接,而是通信双方都保存了对端的信息,而UDP呢,它是每发送一次数据,都要指定一次对端的IP和端口号,然后将生成的数据报作为send的参数发出去,下图就是UDP中的发送数据报的核心代码;

在这里插入图片描述

对于TCP来说,并不需要,前提是需要先把连接给建立上。而这个连接如何建立,是不需要我们通过代码来实现的,而是操作系统的内核自动负责完成的,对于客户端这边来说,主要是“发起连接”动作,在服务器这边的应用程序是没有任何感知的,因为,系统内核就直接完成了建立连接的流程(这个流程就是三次握手),完成这个建立连接的流程之后,建立好的连接就会放在内核中的一个队列里排队(每一个ServerSocket都有这样的队列),服务器这边的应用程序想要和客户端进行通信,就会通过accept()方法,将建立好的连接对象拿到应用程序中,这样就可以进行通信,如果队列中没有建立好的连接的话,accept()方法就会阻塞等待,这个队列呢就是一种生产者消费者模型。

下面通过两个示例来讲解这些方法的使用:

示例:回显服务器

服务器端

一:建立连接

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        //创建服务器端的Socket对象
        socket = new ServerSocket(port);
    }
    //启动服务器方法
    public void start() throws IOException {
        System.out.println("服务器启动");

        //1.通过accept方法将建立好的连接拿到应用程序中,如果没有,则阻塞等待
        Socket clientSocket = socket.accept();

        //通过这个方法,处理拿到的连接
        process_connection(clientScoket);
    }

二:计算请求,返回响应

    private void process_connection(Socket clientSocket){
        //打印连接上的对端信息
        System.out.printf("客户端上线:[%s:%d]\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());

        //通过clientSocket中的流对象来使双方进行通信
        try(InputStream is = clientSocket.getInputStream();
            OutputStream os = clientSocket.getOutputStream()) {

            //因为服务器和客户端可能不止一次进行交互,所以通过while进行循环交互
            while(true) {
                //通过Scanner的方式将流对象中的请求数据读取出来
                Scanner scanner = new Scanner(is);
                //判断scanner是否有下一个数据,如果没有,连接结束
                if(!scanner.hasNext()) {
                    System.out.printf("客户端下线:[%s:%d]\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //拿到客户端请求,通过next()方法,约定读到"\n"结束
                String request = scanner.next();

                //通过这个方法根据请求计算相应
                String response = calculator_response(request);

                //返回服务器处理的响应
                //2.通过PrintWriter进行发送

               PrintWriter printWriter = new PrintWriter(os);
                //这里的println可不是打印到控制台,而是发送给OutputStream对象,然后发送给客户端
                printWriter.println(response);

                //通过flush方法刷新缓冲区,如果没有这个操作的话,写入的数据很可能会在缓冲区中,而没有被发送出去
                printWriter.flush();

                //打印一下这次交互的内容
                System.out.printf("[%s:%d], request:%s, response:%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 calculator_response(String request) {
        //因为我们写的是回显服务器,所以请求是什么,响应就是什么
        return request;
    }

    public static void main(String[] args) {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9999);
        try {
            tcpEchoServer.start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

在这里插入图片描述

在这里插入图片描述

客户端

一、建立连接

public class TcpEchoClient {

    private Socket socket = null;
    public TcpEchoClient(String ip, int port) {
        try {
            //指定服务器的地址和端口号
            //当我们new这个对象时,系统内核就会自动帮我们完成建立连接的流程;
            socket = new Socket(ip, port);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

二、发起请求,获取响应

 //客户端启动方法
    public void start() {

            try(InputStream is = socket.getInputStream();
                OutputStream os = socket.getOutputStream()) {
                Scanner scanner = new Scanner(System.in);
                PrintWriter printWriter = new PrintWriter(os);
                Scanner in = new Scanner(is);
                while(true) {

                    //1.向服务器发起请求
                    System.out.println("请输入请求->:");
                    String request = scanner.next();
                    printWriter.println(request);
                    printWriter.flush();

                    //2.读取服务器返回来的响应
                    String response = in.next();
                    System.out.println("response:" + response);
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    }

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

运行之后:

在这里插入图片描述

在这里插入图片描述

但是,上述代码还存在一个问题,现在是一个服务器和一个客户端进行交互,如果多个客户端和一个服务器进行交互,那么这个bug就出来了!!!

下图为IDEA如何同时运行多个进程的操作步骤

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

同时运行多个客户端,分析一下代码会出现什么问题!!!

运行完之后,可以看到,虽然启动了两个客户端,但是,在服务器这边,并没有显示第二个客户端上线,这是为什么呢?下面就结合代码分析一下原因

在这里插入图片描述

在这里插入图片描述

如果想要在处理第一个客户端请求的过程中,accept快速的接收到第二个客户端建立起的连接,那么,就需要使用到并发编程!!!

利用线程池实现并发编程

利用线程池的方式分配一个新的线程去执行process_connection方法中的处理请求的逻辑,让主线程继续去获取到系统队列中已经建立好连接的其他客户端,然后再通过线程池分配新的线程去执行,利用这样的方式,就可以实现多个客户端与服务器进行交互。

    public void start() throws IOException {
        System.out.println("服务器启动");
        //创建一个线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while(true) {
            //1.通过accept方法将建立好的连接拿到应用程序中
            Socket clientSocket = socket.accept();

            //通过这个方法,使用新的线程处理拿到的连接
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    process_connection(clientSocket);
                }
            });
        }
    }

在这里插入图片描述

文章来源:https://blog.csdn.net/Shine0115/article/details/134888143
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。