【网络编程】TCP流套接字编程(TCP实现回显服务器)

一.TCP流套字节相关API.

Socket(既能给客户端使用,也能给服务器使用)

构造方法
基本方法:

ServerSocket(只能给服务器使用)

构造方法:
基本方法:

二.TCP实现回显服务器.

客户端代码示例:

bash 复制代码
package Demo2;

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 clientSocket =null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //此处可以把这里的IP和port直接传给socket对象.
        //由于TCP是有连接的,所以socket中就会保存好这两个信息.
        clientSocket = new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("客户端启动~~");
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()
        ) {
            Scanner scannerConsole = new Scanner(System.in);
            //从控制台读取数据
            Scanner scannerNetWork = new Scanner(inputStream);
            //
            while(true){
                //1.从控制台读取数据.
                System.out.println("->");
                if(!scannerConsole.hasNext()){
                    break;
                }

                String request = scannerConsole.next();
                PrintWriter printWriter = new PrintWriter(outputStream);
                //2.把请求发送给服务器. 这里要使用println来发送.为了让发送的请求末尾带有一个换行.
                printWriter.println(request);
                //通过flush来主动刷新缓冲区,来确保数据发送到服务器了.
                printWriter.flush();
                //3.从服务器读取响应.这里也是和服务器返回响应的逻辑想对应
                String response = scannerNetWork.next();
                //4.把响应打印到控制台.
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

服务器代码示例:

bash 复制代码
package Demo2;

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 pool = Executors.newCachedThreadPool();
        while(true) {
            //通过accept方法来接听电话,然后才能进行通信.
            Socket clientSocket = serverSocket.accept();
//            Thread thread = new Thread(()->{
//                processConnection(clientSocket);
//            });
//            thread.start();
            pool.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()
        ) {
            Scanner scanner = new Scanner(inputStream);
            while(true){
                //可以通过inputStream来读取数据了.
                //byte[] buffer = new byte[4096];
                //int n = inputStream.read(buffer);
                //此处读操作完全可以用read来完成,但是read是把读取到的数据放到一个byte数组之中
                //后续根据请求处理响应,还需要把数组转化成字符串.
                //此时就可以使用Scanner来简化这个过程.
                if(!scanner.hasNext()){
                    //读取完毕,例如客户端断开链接.
                    System.out.printf("[%s %d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析,此时有一个隐藏的约定,next读的时候要读到空白符才会结束
                //  因此就要求客户端发来的请求必须带有空白符结尾.比如带有/n或" ".
                String request = scanner.next();
                //2.根据请求计算响应.
                String response = process(request);
                //3.把相应给客户端.
                //outputStream.write(response.getBytes(),0,response.getBytes().length);
                //  通过这种方式可以返回,但是这种方式不方便给返回的响应中添加换行
                //  此时就可以给outputStream套一层来完成更方便的写入.
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s %d] request : %s ;response : %s ",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
                System.out.println();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public String process(String request){
        return request;
    }

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

运行结果:

代码执行流程:

  1. 服务器启动,阻塞在accept,等待客户端建立连接.
  2. 客户端启动.这里的new操作会触发和服务器之间建立连接的操作.此时服务器就会从accept中返回.
  3. 服务器解除阻塞,继续向下执行,执行processConnection方法
    执行这个方法,执行到hasNext就会阻塞,此时虽然建立了连接,但是客户端还没有发来任何请求.hasNext阻塞等待到请求到达.
  4. 客户端继续执行到hasNext,等待用户向客户端写入内容.
  5. 如果用户真的输入了,就会继续向下执行发送请求等待返回的逻辑.
    这里就会把请求真的发出去,同时客户端等待服务器返回响应,此时next就会阻塞等待.
  6. 服务器从hasNext 返回读取到的请求,构造响应,并把响应返回给客户端.
    此时服务器结束此次循环,开启下一次循环,继续阻塞在hasNext等待下一个请求
  7. 客户端读取到响应,并显示出来.
    此时客户端就会结束此次循环,开启下一次循环,继续阻塞在hasNext等待用户输入下一个请求.

代码注意事项:

    1. flush()方法存在一个内存缓冲区.由于文件IO的操作比较低效,因此就希望IO的次数少一些,等攒到一定程度再进行IO操作.(相当于多次IO合并成一次了). 因此就引入了缓冲区,此时就会出现问题,你输入的数据比较少,数据被存在内存缓冲区了,所以需要我们手动刷新缓冲区.
    1. 如果客户端非常的多,就需要创建多个Socket对象,此时就可能导致系统的资源使用完了,因此需要在Socket执行完毕之后关闭资源.
    1. 引入线程池来解决频繁的创建销毁线程.
    1. 如果有多个客户端建立请求,并且长时间不销毁
    • 解决方案一:引入协程===>轻量级线程,用户态可以通过手动调度的方式让一个线程并发的做多个任务.
    • 解决方案二:IO多路复用===>这是一个系统内核级别的机制,本质上是让一个线程去处理多个Socket对象 (这些Socket数据并非是同一时刻都需要处理).
相关推荐
xmh-sxh-13145 分钟前
jdk各个版本介绍
java
Koi慢热8 分钟前
路由基础(全)
linux·网络·网络协议·安全
soulteary20 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
天天扭码24 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶24 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺29 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序36 分钟前
vue3 封装request请求
java·前端·typescript·vue
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django
小码的头发丝、1 小时前
Spring Boot 注解
java·spring boot