22. 网络编程(2)——TCP 协议

Java
267
0
0
2022-11-28

网络编程需要依靠Socket API,在java标准库中有两种风格: 1.(UDP)DatagramSocket:面向数据报(发送接收数据,必须以一定的数据报为单位进行传输) 2.(TCP)ServerSocket:面向字节流

UDP和TCP就是传输层的两个最重要的协议

TCP

服务器逻辑:

1.初始化服务器 2.进入主循环 1)先去从内核中获取到一个TCP的连接 2)处理这个TCP的连接 a)读取请求并解析 b)根据请求计算响应 c)把响应写回给客户端

服务器实现:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    //1.初始化服务器 
    //2.进入主循环 
    //  1)先去从内核中获取到一个TCP的连接 
    //  2)处理这个TCP的连接 
    //    a)读取请求并解析 
    //    b)根据请求计算响应 
    //    c)把响应写回给客户端

    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //  1)先去从内核中获取到一个TCP的连接 
            //TCP的连接管理是由操作系统内核来管理的(先描述,再组织【使用一个阻塞队列来组织若干个连接对象】) 
            //当连接建立成功,内核已经把这个连接对象放到了阻塞队列中了,代码中调用到accept就是从阻塞队列中取出一个连接对象 
            //在应用程序中就是Socket对象 
            //如果服务器启动后,没有客户端建立连接,此时代码中的accept就会阻塞,直到有客户建立连接了才停止阻塞 
            Socket clientSocket = serverSocket.accept();

            //  2)处理这个TCP的连接
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //clientSocket.getInetAddress().toString():获得出IP 
        //clientSocket.getPort()):获得端口号 
        //通过 clientSocket 来和客户端交互,先做好准备工作,获取到clientSocket中流对象 
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            //getInputStream();getOutputStream():字节流 
            //InputStreamReader;OutputStreamWriter:把字节流转成字符流, 
            //BufferedReader;BufferedWriter:套上缓冲区

            //此处是长连接版本:一次连接的过程中,需要处理多个请求和响应 
            //短连接就是去掉while循环 
            while (true) {
                //    a)读取请求并解析 
                String request = bufferedReader.readLine();
                //此处暗含一个信息(协议): 
                //客户端发的数据必须是一个按行发送的数据(每一条数据占一行)

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

                //    c)把响应写回给客户端(客户端要按行来读)
                bufferedWriter.write(response+"\n");
                bufferedWriter.flush();

                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
                    clientSocket.getPort());
        }
    }

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

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


}

这里的服务器方法实现中使用到了长连接,那么对应的就是短连接 长连接:一个连接中,客户端和服务器之间交互N次,直到满足一定条件在断开 短连接:一个连接中,客户端和服务器之间交互一次,交互完毕就断开连接

长连接比短连接效率更高

客户端逻辑:

1.启动客户端(一定不要绑定端口号) 2.进入主循环 a)读取用户输入内容 b)构造一个请求发送给服务器 c)读取服务器的响应数据 d)把响应数据显示到界面上

客户端:

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

public class TcpEchoClient {
    //1.启动客户端(一定不要绑定端口号) 
    //2.进入主循环 
    //  a)读取用户输入内容 
    //  b)构造一个请求发送给服务器 
    //  c)读取服务器的响应数据 
    //  d)把响应数据显示到界面上

    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){
            while (true){
                //  a)读取用户输入内容
                System.out.println("->");
                String request = scanner.nextLine();
                if ("exit".equals(request)){
                    break;
                }

               //  b)构造一个请求发送给服务器
                bufferedWriter.write(request + "\n");//按行写
                bufferedWriter.flush();
                
                //  c)读取服务器的响应数据 
                String response = bufferedReader.readLine();

                //  d)把响应数据显示到界面上
                System.out.println(response);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

img

img

存在的问题

以上的服务器和客户端交互的过程中,第一个客户端发送请求,就会进入while循环,只有当第一个客户端退出的时候,第二个客户端发送的请求才会被响应,其原因就是客户端大于一个的时候,就会在accept方法中阻塞,这时,为了提高效率,也就是说为了让多个客户端一起被服务器响应,就可以利用多线程的方式

代码如下:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpThreadEchoServer {
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            //针对这个连接,单独创建一个线程负责处理 
            Thread t = new Thread(new Runnable() {
                @Override 
                public void run() {
                    processConnection(clientSocket);
                }
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            while (true) {
                //    a)读取请求并解析 
                String request = bufferedReader.readLine();

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

                //    c)把响应写回给客户端(客户端要按行来读)
                bufferedWriter.write(response+"\n");
                bufferedWriter.flush();

                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
                    clientSocket.getPort());
        }
    }

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

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

}

(只有start()方法变了,其他均与之前代码一样)

但是这也会存在一个问题,如果客户端太多了,那么创建的线程也太多了,服务器需要频繁的创建和销毁线程,这时就可以使用标准库中的线程池

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

public class TcpThreadPoolEchoServer {
    private ServerSocket serverSocket = null;

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

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

        //先创建一个线程池 
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override 
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            while (true) {
                //    a)读取请求并解析 
                String request = bufferedReader.readLine();

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

                //    c)把响应写回给客户端(客户端要按行来读)
                bufferedWriter.write(response+"\n");
                bufferedWriter.flush();

                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
                    clientSocket.getPort());
        }
    }

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

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