TCP stream socket programming

Insert image description here

Preface

Earlier we learned to use UDP datagrams to implement socket programming. Because there are some differences between UDP and TCP, there will also be some different approaches when implementing TCP stream socket programming. In this article, I will share with you how to Implement socket programming using TCP streams.

Comparison of characteristics between TCP and UDP

If you want to implement socket programming for TCP streams, you need to first know the difference between TCP and UDP.

  1. TCP is connected and UDP is connectionless.
  2. TCP is reliable transmission and UDP is unreliable transmission.
  3. TCP is byte stream oriented, UDP is datagram oriented.
  4. Both TCP and UDP are full duplex.

The difference between TCP stream socket programming and UDP datagram socket programming implemented here is mainly reflected in the fact that TCP has connections and UDP has no connections; TCP is oriented to byte streams and UDP is oriented to datagrams.

TcpEchoServer server implementation

1. Create the ServerSocket class to establish a connection between the two communicating parties.

To achieve TCP communication, both communicating parties first need to establish a connection. In Java, TCP both parties rely on ServerSocketthe interface to establish a connection. ServerSocketThe bottom layer is similar to a blocking queue . When the client and server establish a connection, ServerSocketthese established connections will be stored sequentially through the kernel. When the two parties need to communicate, the server will This connection will be taken out of the kernel and communicated.

public class TcpEchoServer {
    
    
    ServerSocket serverSocket = null;
    
    public TcpEchoServer(int port) throws IOException {
    
    
    	//服务端需要指定端口号
        serverSocket = new ServerSocket(port);
    }
}

2. Take out the established connection to realize communication between the two parties

accept()The established connection can be retrieved through the method for communication between the two parties.

public void start() throws IOException {
    
    
	System.out.println("服务端启动");
    while (true) {
    
    
        Socket clientSocket = serverSocket.accept();
        //进行通信
        processConnection(clientSocket);
    }
}

3. Server-side business logic implementation

TCP is byte stream oriented transmission, unlike UDP which relies on datagram transmission, so here you need to use the InputStreamand in the previous file operation OutputStreamto realize the reading of the request and the sending of the response.

public void processConnection(Socket clientSocket) {
    
    
	//提示客户端上线
    System.out.printf("[%s %d] 客户端线\n",clientSocket.getInetAddress(),
            clientSocket.getPort());
    try (InputStream inputStream = clientSocket.getInputStream();
         OutputStream outputStream = clientSocket.getOutputStream()) {
    
    
            
    } catch (IOException e) {
    
    
        throw new RuntimeException(e);
    }
}

Reading byte data can be cumbersome, so you can use Scannerto simplify the reading process.
scanner.next()The method reads until the end of the whitespace character (space, carriage return, tab character), which corresponds to the end of the whitespace character when our client inputs the request.

Scanner scanner = new Scanner(inputStream);
while (true) {
    
    
	if (!scanner.hasNext()) {
    
    
        System.out.printf("[%s %d] 客户端下线",clientSocket.getInetAddress(),
        clientSocket.getPort());
        break;
    }
    String request = scanner.next();
}

process(String request)Method implements processing of request data.

String response = process(request);

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

When the server receives process()the data returned by the method, it needs to return the processed data to the server. Because it is also oriented to byte streams, it needs to rely on the OutputStream class to transmit the data to the server. However, byte stream operations are more troublesome. , so you can use PrintWriterthe class to encapsulate OutputStreamthe class to simplify the operation.

PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
//打印出服务端日志
System.out.printf("[%s %d] req=%s res=%s\n",clientSocket.getInetAddress(),
                clientSocket.getPort(),request,response);

close resource

When implementing socket programming for TCP streams, resource leakage will occur if the resources are not closed. So why does resource leakage occur? When the server and the new client realize mutual communication, the serverSocket.accept()method is called to create a new Socketresource, and this Socketresource does not always run through the server program, so the resource needs to be closed in time to prevent resource leakage.

But, don’t we use try-with-resourcesa model? When the code in this model is executed, won’t it automatically call closethe methods of the objects inside? Yes, there is no problem here, but what we create in this model is InputStreaman object and OutputStreaman object. When the program ends, the stream object is closed, but Socketthe object is not closed, so we still need to manually close this Socketresource.

What we use here is finallya code block to close the resource to prevent the resource from being closed successfully due to exceptions in the intermediate code.

finally {
    
    
    clientSocket.close();
}

Overall server code

package netWork;

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 {
    
    
    ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
    
    
        //服务端需要指定端口号
        serverSocket = new ServerSocket(port);
    }
    
    public void start() throws IOException {
    
    
        System.out.println("服务端启动");
        while (true) {
    
    
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    public void processConnection(Socket clientSocket) throws IOException {
    
    
        System.out.printf("[%s %d] 客户端上线\n",clientSocket.getInetAddress(),
                clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
    
    
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
    
    
                if (!scanner.hasNext()) {
    
    
                    System.out.printf("[%s %d] 客户端下线",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                String response = process(request);
                printWriter.println(response);
                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 {
    
    
            clientSocket.close();
        }
    }

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

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


TcpEchoClient client implementation

1. Create a Socket object to communicate with the server

The communication between the client and the server relies on Socketthe interface.

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

Although in TCP communication, both parties will save each other's information, but we still need to specify the destination IP and destination port in the client code .

2. Implement the main logic of the client

Prompts the user for request data.

public void run() {
    
    
    Scanner scanner = new Scanner(System.in);
    try (InputStream inputStream = socket.getInputStream();
         OutputStream outputStream = socket.getOutputStream()) {
    
    
        Scanner netWorkScanner = new Scanner(inputStream);
        while (true) {
    
    
            String request = scanner.next();
        }
    } catch (IOException e) {
    
    
        throw new RuntimeException(e);
    }
 }

Transfer the requested data via OutputStream.

PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();

Use InputStreamto read the data returned by the server.

Scanner netWorkScanner = new Scanner(inputStream);
String response = netWorkScanner.next();
System.out.println(response);

Overall server code

package netWork;

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 服务器在哪里~~
        // 具体建立连接的细节, 不需要咱们代码手动干预. 是内核自动负责的.
        // 当我们 new 这个对象的时候, 操作系统内核, 就开始进行 三次握手 具体细节, 完成建立连接的过程了.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
    
    
        // tcp 的客户端行为和 udp 的客户端差不多.
        // 都是:
        // 3. 从服务器读取响应.
        // 4. 把响应显示到界面上.
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
    
    
            PrintWriter writer = new PrintWriter(outputStream);
            Scanner scannerNetwork = new Scanner(inputStream);
            while (true) {
    
    
                // 1. 从控制台读取用户输入的内容
                System.out.print("-> ");
                String request = scanner.next();
                // 2. 把字符串作为请求, 发送给服务器
                //    这里使用 println, 是为了让请求后面带上换行.
                //    也就是和服务器读取请求, scanner.next 呼应
                writer.println(request);
                writer.flush();
                // 3. 读取服务器返回的响应.
                String response = scannerNetwork.next();
                // 4. 在界面上显示内容了.
                System.out.println(response);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

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

Function realization

Insert image description here
Insert image description here

Multiple clients access the server

Since it is network programming, it must be inseparable from multiple clients accessing a server at the same time. If we want to use multiple clients to access the same server at the same time, is it possible?

How to implement two processes of the same code in IDEA?

Insert image description here
Insert image description here
Insert image description here

Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here

Insert image description here
When multiple clients access this server at the same time, we can find that this function is not really implemented. So why can't this code enable multiple clients to access the same server at the same time?

By observing the server-side code, we can find that: there are actually two while loops here . The first loop is used to obtain connections with multiple clients, but why is there no connection with our second client? ? When the first client here is communicating with the server, it is in the second while loop, and the first client here has not disconnected from the server. In other words, the current server is still in the second while loop. The two while loops wait for the first client to send a request, so when the second client wants to communicate with the server, it cannot reach serverSocket.accept()the method to communicate with the server. So how to solve this problem?

This requires the use of the multi-threading knowledge we learned earlier. Create multiple threads to perform processConnection()methods, so that when another client wants to communicate with the server, it can get the information from the server in another thread. connect.

public void start() throws IOException {
    
    
    System.out.println("服务端启动");
    while (true) {
    
    
        Socket clientSocket = serverSocket.accept();
        Thread t = new Thread(() -> {
    
    
            try {
    
    
                processConnection(clientSocket);
            } catch (IOException e) {
    
    
                throw new RuntimeException(e);
            }
        });
        t.start();
    }
}

Insert image description here
Insert image description here
Insert image description here

However, such frequent creation and destruction of threads will also consume more resources, so optimization can be made here: use a thread pool. Although the thread pool can be optimized here, there is not much optimization.

public void start() throws IOException {
    
    
    System.out.println("服务端启动");
    ExecutorService service = Executors.newCachedThreadPool();
    while (true) {
    
    
        Socket clientSocket = serverSocket.accept();
        service.submit(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    processConnection(clientSocket);
                } catch (IOException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
    }
 }

In fact, the best way to handle multiple clients accessing the same server at the same time is IO multiplexing/IO multiplexing .

IO multiplexing is a synchronous IO model that allows multiple IO requests to be processed simultaneously within a single process/thread. Specifically, a process/thread can monitor multiple file handles. Once a file handle is ready, it can notify the application to perform corresponding read and write operations. If no file handle is ready, the application will block and hand over the CPU.
This model allows multiple requests to share the same process/thread, allowing for more efficient use of system resources when handling a large number of requests. If each request is processed using an independent process/thread, the system needs to create and manage a large number of processes/threads, which will consume a lot of system resources. Using IO multiplexing technology, one or several threads can be reused to handle multiple TCP connections, thus greatly reducing system overhead.
The emergence of IO multiplexing technology is mainly to solve the problem of blocking IO. In the operating system, initially there is only BIO mode, which is blocking IO. When a request is processed, if the request needs to wait for an IO operation to complete (such as a read or write operation), the process/thread will be blocked until the IO operation is completed. This can lead to a waste of system resources, especially if a large number of requests need to be processed.
The introduction of IO multiplexing technology solves this problem. By monitoring multiple file handles, one process/thread can handle multiple IO requests at the same time, thus improving the efficiency of the system. When a file handle is ready, the process/thread can immediately perform corresponding read and write operations without waiting for other requests to complete. This method can effectively reduce the waste of system resources and improve system throughput.
IO multiplexing technology is widely used in network programming, especially server-side programming. Since the server needs to handle requests from a large number of clients at the same time, using IO multiplexing technology can improve the performance and response speed of the server. For example, the Nginx server uses IO multiplexing technology to handle a large number of client connections.

I won’t go into details about how to solve the problem here. If you are interested, you can learn more.

Optimized server code

package netWork1;

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
    
    
            // 通过 accept 方法, 把内核中已经建立好的连接拿到应用程序中.
            // 建立连接的细节流程都是内核自动完成的. 应用程序只需要 "捡现成" 的.
            Socket clientSocket = serverSocket.accept();
            // 此处不应该直接调用 processConnection, 会导致服务器不能处理多个客户端.
            // 创建新的线程来调用更合理的做法.
            // 这种做法可行, 不够好
//            Thread t = new Thread(() -> {
    
    
//                processConnection(clientSocket);
//            });
//            t.start();

            // 更好一点的办法, 是使用线程池.
            service.submit(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    processConnection(clientSocket);
                }
            });
        }
    }

    // 通过这个方法, 来处理当前的连接.
    public void processConnection(Socket clientSocket) {
    
    
        // 进入方法, 先打印一个日志, 表示当前有客户端连上了.
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
        // 接下来进行数据的交互.
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
    
    
            // 使用 try ( ) 方式, 避免后续用完了流对象, 忘记关闭.
            // 由于客户端发来的数据, 可能是 "多条数据", 针对多条数据, 就循环的处理.
            while (true) {
    
    
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
    
    
                    // 连接断开了. 此时循环就应该结束
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                // 1. 读取请求并解析. 此处就以 next 来作为读取请求的方式. next 的规则是, 读到 "空白符" 就返回.
                String request = scanner.next();
                // 2. 根据请求, 计算响应.
                String response = process(request);
                // 3. 把响应写回到客户端.
                //    可以把 String 转成字节数组, 写入到 OutputStream
                //    也可以使用 PrintWriter 把 OutputStream 包裹一下, 来写入字符串.
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    此处的 println 不是打印到控制台了, 而是写入到 outputStream 对应的流对象中, 也就是写入到 clientSocket 里面.
                //    自然这个数据也就通过网络发送出去了. (发给当前这个连接的另外一端)
                //    此处使用 println 带有 \n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据.
                printWriter.println(response);
                //    此处还要记得有个操作, 刷新缓冲区. 如果没有刷新操作, 可能数据仍然是在内存中, 没有被写入网卡.
                printWriter.flush();
                // 4. 打印一下这次请求交互过程的内容
                System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                // 在这个地方, 进行 clientSocket 的关闭.
                // processConnection 就是在处理一个连接. 这个方法执行完毕, 这个连接也就处理完了.
                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(9906);
        server.start();
    }
}

Implement a simple dictionary function based on the echo server

The business logic of this function server is a little more complicated than simple echo, but it is not much more complicated. You just need to change the processmethod of the server. In this method, you Mapcan use to store the translations of a few words.

package netWork;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class TcpDictServer extends TcpEchoServer {
    
    

    Map<String, String> dict = new HashMap<>();
    public TcpDictServer(int port) throws IOException {
    
    
        super(port);
        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("bird","小鸟");
        dict.put("pig","小猪");
    }

    @Override
    public String process(String request) {
    
    
        return dict.getOrDefault(request,"您查找的单词在该词典中不存在");
    }

    public static void main(String[] args) throws IOException {
    
    
        TcpDictServer tcpDictServer = new TcpDictServer(9906);
        tcpDictServer.start();
    }
}

Insert image description here
Insert image description here

Guess you like

Origin blog.csdn.net/m0_73888323/article/details/133907200