网络编程—Socket套接字(TCP)

发布于:2025-04-09 ⋅ 阅读:(37) ⋅ 点赞:(0)

上篇文章:

网络编程—Socket套接字(UDP)https://blog.csdn.net/sniper_fandc/article/details/146923670?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923670&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

目录

1 TCP流套接字

2 模拟实现TCP服务器


1 TCP流套接字

        基于TCP的Socket主要有:ServerSocket和Socket,ServerSocket用于创建TCP服务器端的Socket,而Socket用于创建TCP客户端的Socket。操作方式也类似文件。

构造方法/方法

含义

ServerSocket(int port)

构造方法,创建一个服务端流套接字Socket,并绑定到指定端口

Socket accept()

普通方法,开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待

void close()

关闭TCP套接字

        因为TCP是面向流的数据读写方式,因此没有像DatagramPacket数据报的API,只需创建Socket后,采用类似InputStream和OutputStream的操作方式。也可以对InputStream和OutputStream进行Scanner和PrintWriter的包装,便于字符数据的读写。

构造方法/方法

含义

Socket(String host, int port)

构造方法,创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

InetAddress getInetAddress()

从套接字中获取连接的IP地址

InputStream getInputStream()

返回套接字中的输入流(读请求)

OutputStream getOutputStream()

返回套接字中的输出流(写响应)

        注意:Socket可能有两种获得的方式,1是使用Socket构造方法,2是使用ServerSocket的方法accept()。也就是说ServerSocket的主要作用就是创建TCP服务器的全局连接监听,客户端作为连接发起方,因此直接创建Socket表示申请建立连接,而ServerSocket的accept()方法一旦监听到有客户端申请建立连接,就返回一个Socket用于建立服务器和客户端之间的连接。

        上述分析方式也透露了ServerSocket和Socket的生命周期,ServerSocket的生命周期伴随整个服务器进程,而Socket的生命周期只是一次连接周期。

2 模拟实现TCP服务器

public class TcpServer {

    //服务器端口号

    private final int PORT = 8000;

    //创建服务器

    private ServerSocket serverSocket = null;

    public TcpServer() throws IOException {

        serverSocket = new ServerSocket(PORT);

    }

    //启动服务器

    public void start() throws IOException {

        System.out.println("服务器启动成功");

        ExecutorService executorService = Executors.newCachedThreadPool();

        while(true){

            //将建立的TCP连接拿到应用程序中(accept()会阻塞,直到建立连接)

            Socket clientSocket = serverSocket.accept();

            //[版本1]直接调用processConnect()就会导致第一个客户端连接执行到该方法while中,服务器线程从而无法执行accept

            //进而无法一个服务器为多个客户端服务

            //[版本2]解决方案:多线程(一个线程accept(),一个线程processConnect())(新的问题:频繁创建销毁线程)

//            Thread t = new Thread(() ->{

//                try {

//                    processConnect(clientSocket);

//                } catch (IOException e) {

//                    e.printStackTrace();

//                }

//            });

//            t.start();

            //[版本3]解决方案:线程池(新的问题:线程数量太多了(IO多路复用->NIO))

            executorService.submit(new Runnable() {

                @Override

                public void run() {

                    try {

                        processConnect(clientSocket);

                    } catch (IOException e) {

                        e.printStackTrace();

                    }

                }

            });

        }

    }

    //给当前连接的客户端提供服务(一个连接只进行一次数据交互服务(短连接)||一个连接进行多次数据交互服务(长连接))

    //长连接版本(去掉循环就是短连接版本)

    public void processConnect(Socket clientSocket) throws IOException {

        System.out.printf("[%s:%d] 建立连接\n",clientSocket.getInetAddress().toString(),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] 断开连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());

                    break;

                }

                //1.读取请求并解析

                String request = scanner.next();

                //2.根据请求计算响应

                String response = process(request);

                //3.响应写回客户端

                // (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)

                printWriter.println(response);

                //刷新一下缓冲区

                printWriter.flush();

                System.out.printf("[%s:%d] request:%s, response:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()

                    ,request,response);

            }

        }finally {

            //连接用完需要关闭(clientSocket生命周期是一次连接周期,而serverSocket生命周期是整个服务器运行周期)

            clientSocket.close();

        }

    }



    public String process(String request) {

        return request;

    }



    public static void main(String[] args) throws IOException {

        TcpServer tcpServer = new TcpServer();

        tcpServer.start();

    }

}

public class TcpClient {

    //创建客户端

    private Socket socket = null;

    public TcpClient() throws IOException {

        //new对象时就是和TCP服务器建立连接(因此需要直到服务器地址)

        socket = new Socket("127.0.0.1",8000);

    }

    //启动服务器

    public void start() throws IOException {

        Scanner scanner = new Scanner(System.in);

        try(InputStream inputStream = socket.getInputStream();

            OutputStream outputStream = socket.getOutputStream()){

            Scanner scannerNet = new Scanner(inputStream);

            PrintWriter printWriter = new PrintWriter(outputStream);

            while(true){

                //1.读取用户输入

                System.out.print(">");

                //注意此时next()读取到换行就结束了,但是读取的数据不含空白符,即没有回车符

                String request = scanner.next();

                //2.发送请求

                // (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)

                printWriter.println(request);

                printWriter.flush();

                //3.接收响应

                String response = scannerNet.next();

                //4.将响应返回给用户

                System.out.printf("request:%s, response:%s\n",request,response);

            }

        }

    }



    public static void main(String[] args) throws IOException {

        TcpClient tcpClient= new TcpClient();

        tcpClient.start();

    }

}

运行结果如下:

        上述代码需要注意3点:

        1.服务器端什么时候该关闭clientSocket(即关闭连接)?当服务器端processConnect方法内部从循环跳出时,证明此时客户端没有数据要发送,此时可以关闭连接,采用try-catch-finally方式,防止出现异常无法正常关闭。

        2.如何处理next()引起的阻塞问题?上述代码很多地方可能要用到Scanner的next()方法,但是该方法会读取到空白符(回车换行等)才能结束,当客户端输入数据时可能不会携带空白符(在命令行中敲回车,该回车会被接收数据的next识别,发送的请求中并不携带回车符),此时就会导致服务器端一直未识别到结束,从而一直无响应。解决的办法就是在发送的数据中添加空白符,比如使用println()方法会自动在数据结尾添加回车符。

        3.如何解决服务器端只能为一个客户端服务?当不采用多线程方案时,第一个客户端建立连接发送请求,进入processConnect方法内部时,服务器端的主线程就会进入while中,从而其他客户端申请建立连接时,服务器主线程无法通过accept()监听建立连接的申请。采用多线程方案,线程池实现一个线程为一个客户端服务(注意,当并发量很大时,线程池的线程数量很多,就会导致资源浪费调度困难等问题,此时需要采用NIO(非阻塞IO)的方式,这是一种I/O多路复用的技术,可以实现一个线程管理多个客户端)。

下篇文章:

网络编程—TCP/IP模型(UDP协议与自定义协议)https://blog.csdn.net/sniper_fandc/article/details/146923934?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923934&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link


网站公告

今日签到

点亮在社区的每一天
去签到