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

上篇文章:

网络编程---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流套接字](#1 TCP流套接字)

[2 模拟实现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服务器

java 复制代码
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

相关推荐
写代码的小王吧2 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
musk12123 小时前
wsl2 配置ubuntu 固定ip
linux·tcp/ip·ubuntu
GalaxyPokemon3 小时前
Muduo网络库实现 [七] - Connection模块
linux·服务器·网络
the_nov4 小时前
19.TCP相关实验
linux·服务器·网络·c++·tcp/ip
林中伊人5 小时前
家庭路由器wifi设置LAN2LAN和LAN2WAN
网络·路由器
XYN615 小时前
【嵌入式学习3】基于python的tcp客户端、服务器
服务器·开发语言·网络·笔记·python·学习·tcp/ip
the_nov5 小时前
20.IP协议
linux·服务器·网络·c++·tcp/ip
密码小丑6 小时前
玄机-应急响应-webshell查杀
网络·笔记