【JavaEE】TCP流套接字编程

发布于:2025-04-13 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

API

1.Socket类(客户端)

2.ServerSocket类(服务端)

创建回显服务器-客户端

服务器引入多线程

服务器引入线程池

解疑惑

长短连接


在Java中,TCP流套接字是基于TCP协议实现的网络通信方式,提供面向连接、可靠、有序的双向字节流传输。


API

TCP流套接字的核心API由java.net.Socket(客户端)和java.net.ServerSocket(服务端)组成

1.Socket类(客户端)

构造函数:

·Socket()

创建一个未连接的套接字,需手动调用connect(SocketAddress endpoint)

·Socket(String host,int port)

直接连接指定主机和端口,抛出UnkownHostException或IOException

·Socket(InetAddress address,int port)

使用InetAddress对象指定服务器地址,避免DNS解析

·Socket(String host,int port,InetAddress localAddr,int localPort)

绑定本地地址和端口,用于多网卡环境或指定出口IP

核心方法:

连接管理:

·connect(SocketAddress endpoint):手动建立连接

·isConnected():检查是否已连接

·InetAddress getInetAddress():返回套接字所连接的地址

·close():关闭套接字,释放资源

·数据流获取:

·InputStream getInputStream():获取输入流(读取服务器数据)

·OutputStream getOutputStream():获取输出流(发送数据到服务器)

·参数配置:

·setSoTimeout(int timeout):设置读写超时(毫秒),超时抛出SocketTimeoutException

·setSendBufferSize(int size):设置发送缓冲区大小(默认8KB)

·setReceiveBufferSize(int size):设置接收缓冲区大小(默认8KB)

·setTcpNoDelay(boolean on):禁用Nagle算法(默认false,启用延迟发送以提高数据包效率)

·setKeepAlive(boolean on):启用TCP KeepAlive机制(默认false)

示例代码:

try (Socket socket = new Socket("example.com", 80)) {
    socket.setSoTimeout(5000); // 5秒超时
    socket.setTcpNoDelay(true); // 禁用Nagle算法
    
    OutputStream out = socket.getOutputStream();
    out.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".getBytes());
    
    InputStream in = socket.getInputStream();
    // 读取响应...
}

2.ServerSocket类(服务端)

构造函数

·ServerSocket(int port)

绑定指定端口,默认等待连接队列长度为50

·ServerSocket(int port,int backlog)

设置等待连接队列长度(backlog),避免客户端连接被拒绝

·ServerSocket(int port,int backlog,InetAddress bindAddr)

绑定到特定本地地址(如多网卡服务器指定监听IP)

核心方法

·监听与接受连接:

·accpet():阻塞等待客户端连接,返回Socket对象

·setReuseAddress(boolean on):允许端口在关闭后快速重用(默认false,避免BindException)

·参数配置:

·setSoTimeout(int timeout):设置accpet()方法的超时时间

·isClosed():检查是否已关闭

我们在学习完Socket和ServerSocket后,尝试写一个TCP的回显客户端/服务器

创建回显服务器-客户端

回显服务器--客户端:客户端发送什么请求,服务器便返回什么

回显服务器TcpEchoServer1

public class TcpEchoServer1 {
    ServerSocket serverSocket=null;

    public TcpEchoServer1(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务已启动");
        while(true){
            //服务器启动后 就开始接受客户端连接
            Socket clientSocket=serverSocket.accept();
            //处理接收
            processConnections(clientSocket);
        }
    }

    private void processConnections(Socket clientSocket) throws IOException {
        //打印连接信息
        System.out.printf("[%s:%d]客户端已连接\n",
                clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //读写数据
        try(InputStream inputStream=clientSocket.getInputStream()){
            try(OutputStream outputStream=clientSocket.getOutputStream()){
                //用Scanner处理更方便
                Scanner scanner=new Scanner(inputStream);//传入输入流,读取数据
                //循环获取请求
                while(true){
                    //如果没有下一个数据就结束
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d]客户端断开连接\n",
                                clientSocket.getInetAddress().toString(),
                                clientSocket.getPort());
                        break;
                    }
                    String request=scanner.next();//读取客户端的请求
                    //处理数据
                    String response=process(request);
                    //把处理结果响应给客户端
                    PrintWriter writer=new PrintWriter(outputStream);
                    writer.println(response);
                    writer.flush();
                    //打印日志
                    System.out.printf("[%s:%d]request:%s,response:%s\n",
                            clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //关闭Socket
            clientSocket.close();
        }
    }

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

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

回显客户端TcpEchoClient1

public class TcpEchoClient1 {
    private Socket socket;

    //构造方法指定IP和端口号
    public TcpEchoClient1(String serverIp,int port) throws IOException {
        socket=new Socket(serverIp,port);
    }

    public void start(){
        System.out.println("连接服务器成功");
        //用socket.getInputStream获取
        try(InputStream inputStream=socket.getInputStream()){
            try(OutputStream outputStream=socket.getOutputStream()){
                Scanner scanner=new Scanner(System.in);
                while(true){
                    //1.接收用户输入
                    System.out.println("->");
                    String request=scanner.next();
                    //2.构造数据发送到服务器
                    PrintWriter writer=new PrintWriter(outputStream);
                    //写数据
                    writer.println(request);
//强制刷新缓冲区,如果不刷新,客户端可能不能及时收到响应
                    writer.flush();
                    //3.接收服务器响应的数据
                    Scanner resScanner=new Scanner(inputStream);
                    String response=resScanner.next();
                    //4.解析响应并打印
                    System.out.printf("request:%s,response:%s\n",request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

服务器引入多线程

如果只是单个线程,无法同时响应多个客户端

此处给每个客户端都分配一个线程

客户端不做修改,只是在服务器处理客户端连接时,使用多线程去接收

代码如下:

public class TcpEchoServer2 {
    ServerSocket serverSocket=null;

    public TcpEchoServer2(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务已启动");
        while(true){
            //服务器启动后 就开始接受客户端连接
            Socket clientSocket=serverSocket.accept();
            //处理接收,每建立一个连接就创建一个线程取处理请求
            Thread t=new Thread(()->{
                try {
                    processConnections(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }

服务器引入线程池

为了避免频繁创建销毁线程,也可以引入线程池

这里如上,也只在服务器中进行修改。将创建多线程去接收客户端连接修改成:让线程池去提交请求,分配线程去处理客户端的请求

代码如下

public class TcpEchoServer {
    private ServerSocket serverSocket=null;
    //这里和UDP服务器类似,也是在构造对象的时候,绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");

        //使用线程池管理线程
        ExecutorService executorService= Executors.newCachedThreadPool();

        while(true){
            //对于TCP来说,需要先处理客户端发来的连接
            //通过读写clientSocket,和客户端进行通信
            //如果没有客户端发起连接,此时accept就会阻塞
            //主线程负责进行accept。每次accept到一个客户端,就创建一个线程
            //由新线程负责处理客户端的请求
            Socket clientSocket=serverSocket.accept();
            executorService.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

当大家逐一敲写代码后,可能会产生以下疑问?

解疑惑

1.服务器和客户端如何建立连接?我们这里简单讲解,后续会详细讲解(三次握手)

服务器在启动后,会阻塞等待连接(accpet方法)。

当客户端通过构造方法new Socket创建socket实例后,向服务器的serverSocket发起连接请求。

服务器接受连接,生成clientSocket(Socket clientSocket=serverSocket.accpet() )

通过服务器的clientSocket和客户端的socket进行网络通信

2.客户端从终端输入请求,服务端是如何获取请求的?

我们知道服务器和客户端分别通过clientSocket和socket进行网络通信

客户端发送请求:客户端通过PrintWriter将用户输入 写入输出流

outputStream=socket.getOutputStream();

PrintWriter writer=new PrintWriter(outputStream);

writer.println(request);//将字符串写入输出流

writer.flush();//强制刷新缓冲区,确保数据立即发送

服务端获取请求:

通过服务器的inputStream=clientSocket.getInputStream()获取客户端的输入流

然后根据

Scanner sc=new Scanner(inputStream);

String request=sc.next();

读取客户端的输入流(请求)

3.服务端根据请求返回响应,客户端又是如何获取响应的?

服务端返回响应:服务端也通过PrintWriter将响应 写入输出流

PrintWriter writer=new PrintWriter(outputStream);
String response=process(request);
writer.println(response);
writer.flush();

客户端获取响应:

通过客户端的inputStream=socket.getInputStream()获取服务器的输入流

然后根据

Scanner scanner=new Scanner(inputstream);

String response=scanner.next()

读取服务器的输入流(响应)

4.服务器的serverSocket、clientSocket和客户端的socket三者有何联系?

服务器

serverSocket:服务器端监听套接字

clientSocket:服务器端通信套接字。clientSocket是服务器为每个连接的客户端创建的专用套接字

数据传输:

通过clientSocket.getInputStream和getOutputStream与客户端交换数据

clientSocket在客户端连接时创建,连接断开时关闭

客户端

socket:客户端套接字

·发起连接:通过new Socket(服务器IP,端口)连接到服务器

·数据传输:通过socket.getInputStream和getOutputStream与服务器通信

socket在连接服务器后创建,通信接收后关闭

三者协作关系

1.连接建立

·客户端socket向服务器的serverSocket发起连接请求

·服务器serverSocket接受连接,生成一个clientSocket

2.数据传输

·客户端通过自身socket发送数据-->服务器通过对应的clientSocket接收数据

·服务器通过clientSocket发送响应-->客户端通过自身的socket接收响应

3.连接终止

·任意一端关闭套接字,连接即断开

·clientSocket和客户端socket关闭,但serverSocket继续监听新连接

5.Scanner与流(InputStream、OutputStream)的关系

输出流(OutputStream):用于发送数据(客户端--发送请求--服务端/服务端--返回响应--客户端)

输入流(InputStream):用于接收数据(服务端接收客户端的请求/客户端接收服务端的响应)

长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接:每次接收到数据并返回响应后,就关闭连接,即是短连接。也就是说,短连接只能一次收发数据

长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据

对比以上 长短连接,两者区别如下:

·建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说,建立连接,关闭连接也是要耗时的,长连接效率更高

主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发

两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等

扩展了解

基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的

由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行

一次阻塞等待 对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求

实时应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以得到极大的提升