[Network programming] (TCP stream socket programming ServerSocket API Socket API handwritten TCP version echo server TCP long and short connections)

Article directory

  • network programming
    • TCP stream socket programming
      • ServerSocket API
      • Socket APIs
      • Long and short connections in TCP
      • Handwritten TCP version of the echo server

Network programming

TCP stream socket programming

The API provided by TCP is mainly two classes: ServerSocket and Socket.
TCP does not need a class to represent “TCP datagram” because TCP is not transmitted in units of datagrams. It is streamed in bytes

ServerSocket API

ServerSocket is a Socket object specially used by the server.

ServerSocket construction method:

ServerSocket(int port) Create a server stream socket Socket and bind it to the specified port.

ServerSocket method:

Socketaccept() starts to listen to the specified port (the port bound at the time of creation). After a client connects, it returns a Socket object on the server side, and establishes a connection with the client based on the Socket, otherwise it blocks and waits.

voidclose() closes this socket

Socket API

Socket is used both by the client and by the server.

Socket construction method: on the server side, there is an accept return. On the client side, an IP and port number are specified when constructing in the code. (The IP and port here are the server IP and port) with this information You can connect to the server.

Socket(String host, intport) creates a client stream socket Socket, and establishes a connection with the process of the corresponding port on the host of the corresponding IP.

Socket method:

Further obtain the internal stream object through the Socket object, and use the stream object to send/receive.

InetAddress getInetAddress() returns the address the socket is connected to
InputStream getInputStream() returns the input stream of this socket
OutputStream getOutputStream() returns the output stream of this socket

Long and short connections in TCP

In the scenario where TCP has a connection, there are two typical manifestations of the concept of connection.

  1. Short connection: Every time the client sends a message to the server, it first establishes a connection and sends a request; if it sends it next time, it re-establishes the connection.

  2. Long connection: After the client establishes a connection, the connection is not disconnected first, and then the request is sent again, and the response is read; the request is sent again, and the response is read; after several rounds, the client does not need to use this connection in a short time, Then disconnect again.

The difference between the two:

  1. Time-consuming to establish and close a connection: for a short connection, a connection needs to be established and closed for each request and response; for a long connection, only the first connection needs to be established, and subsequent requests and responses can be transmitted directly. Relatively speaking, it is time-consuming to establish a connection and close the connection, and the long-term connection is more efficient.
  2. The active sending request is different: the short connection is generally the client actively sending the request to the server; the long connection can be the client actively sending the request, or the server actively sending the request.
  3. The usage scenarios of the two are different: short connections are suitable for scenarios where the frequency of client requests is not high, such as browsing web pages. Persistent connections are suitable for scenarios where the client communicates frequently with the server, such as chat rooms and real-time games.

Expand understanding:

Handwritten TCP version echo server

TCP server

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("Start server!");
        while (true) {<!-- -->
            //Use this clientSocket to communicate with a specific client
            Socket clientSocket = serverSocket. accept();
            processConnection(clientSocket);
        }
    }
    //Use this method to process a connection, a connection corresponds to a client
    //May involve multiple interactions
    private void processConnection(Socket clientSocket) {<!-- -->
        //Get client IP and port
        System.out.printf("[%s:%d] client is online!\
",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // Communicate with the client based on the above socket object
        try(InputStream inputStream = clientSocket. getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){<!-- -->
            //Because of processing multiple requests and responses, loops are also used
            while (true){<!-- -->
                //1. Read request
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){<!-- -->
                    //There is no next data, indicating that the reading is over (the client closes the connection)
                    System.out.printf("[%s:%d] client offline!\
",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //The use of next here is to read until the end of a newline/space/other blank
                //But the result does not contain the above blanks
                String request = scanner. next();
                //2. Respond according to the request
                String response = process(request);
                //3. Return the response result
                //outputStream does not have the function of write String, you can take out the byte array in String and write it
                //You can also use character streams to convert
                PrintWriter printWriter = new PrintWriter(outputStream);
                //Use println to write here, so that the result has a \
 line break, which is convenient for the peer to receive and analyze
                printWriter. println(response);
                //flush is used to refresh the buffer to ensure that the currently written data is indeed sent out
                printWriter. flush();
                System.out.printf("[%s:%d] req: %s; resp: %s \
",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request, response);
            }
        }catch (IOException e){<!-- -->
            e.printStackTrace();
        }finally {<!-- -->
            try {<!-- -->
                clientSocket. close();
            } catch (IOException e) {<!-- -->
                e.printStackTrace();
            }
        }
    }
    public String process(String request) {<!-- -->
        return request;
    }
    public static void main(String[] args) throws IOException {<!-- -->
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

TCP client

public class TcpEchoClient {<!-- -->
    private Socket socket = null;

    public TcpEchoClient(String serverIp,int serverPort) throws IOException {<!-- -->
        //The Socket construction method can recognize the IP address in the electrical decimal format, which is more convenient than DatagramPacket
        //While new this object, it will perform TCP connection operation
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){<!-- -->
        System.out.println("Client started!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket. getInputStream();
             OutputStream outputStream = socket. getOutputStream()){<!-- -->
            while (true){<!-- -->
                //1. First read the content entered by the user from the keyboard
                System.out.println("> ");
                String request = scanner. next();
                if(request.equals("exit")){<!-- -->
                    System.out.println("goodbye");
                    break;
                }
                //2. Construct the read content into a request and send it to the server
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter. println(request);
                //Add flush to ensure that the data is indeed sent out
                printWriter. flush();
                //3. Read the server response
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner. next();
                //4. Display the response on the interface
                System.out.println(response);
            }
        } catch (IOException e) {<!-- -->
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {<!-- -->
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

Question:

The current code uses println to send data, and println will automatically bring \
newline after the sent data. If println is not applicable, but use print (without \
newline), can the above code run correctly?

Can not run correctly, without \
is not enough. The TCP protocol is a byte stream-oriented protocol (byte stream characteristics: how many bytes can be read at a time). The receiver cannot know how many bytes we want at a time, which is We need to make an explicit agreement in data transmission. In the code here, it is implicitly agreed to use \
as the request/response split agreement of the current code.

But we found that there are still some problems with the above code:

To solve the above problems, we use multi-threading, the main thread is responsible for accepting. Every time a connection is received, a new thread is created, and this new thread is responsible for processing the new client. Each thread is an independent execution flow. Each independent execution flow executes its own logic and is concurrent with each other. There will be no blocking on one side and affecting the execution on the other side.

If there are too many servers and clients, and many clients frequently establish connections, they need to create/destroy threads frequently. At this time, the simple multi-thread processing method will not work, so we have to use the thread pool for processing.

If there are a lot of clients and the client connection keeps disconnecting for a long time, it will lead to many threads on the machine. If a server has thousands of clients, it must be thousands of threads. This thing is a problem for the machine It is a big burden. At this time, in order to solve the problem of a single machine supporting a larger number of clients, that is, the C10M problem, I found a way to let one thread handle multiple client connections. In order to solve this problem, the bottom layer of the operating system proposed IO multiplexing, IO Multi-way transfer. It is to use the full waiting time to do other things. We arrange a collection for this thread, and this collection contains a bunch of connections. This thread is responsible for monitoring this collection. Which connection has data, the thread will Which connection to handle, although there are many connections, there are always first and last. The operating system provides some native APIs, select, poll, epoll. In Java, a set of NIO classes are provided to encapsulate the above-mentioned multi-channel Reusable API.

After improvement: TCP server code

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("Start server!");
        //Using CachedThreadPool here, it is not appropriate to use FixedThreadPool (the number of threads should not be fixed)
        ExecutorService threadPool = Executors. newCachedThreadPool();
        while (true) {<!-- -->
            //Use this clientSocket to communicate with a specific client
            Socket clientSocket = serverSocket. accept();
            // use multithreading here
            /*Thread t = new Thread(()->{
                processConnection(clientSocket);
            });*/
            // use thread pool
            threadPool. submit(()->{<!-- -->
                processConnection(clientSocket);
            });
        }
    }
    //Use this method to process a connection, a connection corresponds to a client
    //May involve multiple interactions
    private void processConnection(Socket clientSocket) {<!-- -->
        //Get client IP and port
        System.out.printf("[%s:%d] client is online!\
",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // Communicate with the client based on the above socket object
        try(InputStream inputStream = clientSocket. getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){<!-- -->
            //Because of processing multiple requests and responses, loops are also used
            while (true){<!-- -->
                //1. Read request
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){<!-- -->
                    //There is no next data, indicating that the reading is over (the client closes the connection)
                    System.out.printf("[%s:%d] client offline!\
",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //The use of next here is to read until the end of a newline/space/other blank
                //But the result does not contain the above blanks
                String request = scanner. next();
                //2. Respond according to the request
                String response = process(request);
                //3. Return the response result
                //outputStream does not have the function of write String, you can take out the byte array in String and write it
                //You can also use character streams to convert
                PrintWriter printWriter = new PrintWriter(outputStream);
                //Use println to write here, so that the result has a \
 line break, which is convenient for the peer to receive and analyze
                printWriter. println(response);
                //flush is used to refresh the buffer to ensure that the currently written data is indeed sent out
                printWriter. flush();
                System.out.printf("[%s:%d] req: %s; resp: %s \
",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request, response);
            }
        }catch (IOException e){<!-- -->
            e.printStackTrace();
        }finally {<!-- -->
            try {<!-- -->
                clientSocket. close();
            } catch (IOException e) {<!-- -->
                e.printStackTrace();
            }
        }
    }
    public String process(String request) {<!-- -->
        return request;
    }
    public static void main(String[] args) throws IOException {<!-- -->
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}