【网络编程】TCP数据流套接字编程

目录

[一. TCP API](#一. TCP API)

[二. TCP回显服务器-客户端](#二. TCP回显服务器-客户端)

[1. 服务器](#1. 服务器)

[2. 客户端](#2. 客户端)

[3. 服务端-客户端工作流程](#3. 服务端-客户端工作流程)

[4. 服务器优化](#4. 服务器优化)


TCP数据流套接字编程是一种基于有连接协议的网络通信方式


一. TCP API

在TCP编程中,主要使用两个核心类ServerSocketSocket


ServerSocket

ServerSocket类只有服务器会使用,用于接受连接

ServerSocket构造方法:

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket方法:

方法签名 方法说明
Socket accept() 监听端口,如果有客户端连接后,则会返回一个服务端 Socket 对象 如果没有客户端连接,则会进入阻塞等待
void close() 关闭此套接字
  • accept()方法用于接收客户端连接请求并建立正式通信通道
  • accept()方法是接受连接并返回Socket,真正和客户端进行交互的是Socket

Socket

Socket类负责具体的数据传输

  • 客户端一开始就使用Socket进行通信(请求由客户端发起)
  • 服务器在接受客户端建立请求后,返回服务端Socket
  • 在双方建立连接之后,都会使用Socket进行通信

Socket 构造方法:

方法签名 方法说明
Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket 方法:

方法签名 方法说明
InetAddress getInetAddress() 返回的地址(IP和端口)
InputStream getInputStream() 返回输入流
OutputStream getOutputStream() 返回输出流
  • TCP面向字节流,基本传输单位是字节

二. TCP回显服务器-客户端

回显服务器

**回显服务器:**不进行任何的业务逻辑,只是将收到的数据显示出来


1. 服务器

接收连接请求

  • TCP是有连接的可靠通信
  • 真正建立连接的过程在内核中被实现,应用层只是调用相应API同意建立连接
  • 类比打电话,客户端拨号,服务器这边在响铃,通过调用accept接听

代码实现:

java 复制代码
            Socket clientSocket = serverSocket.accept();
  • accept()方法具有阻塞功能
  • accept()方法一次只能返回一个Socket对象,接收一次请求
  • 如果没有客户端发起连接请求,则会进入阻塞等待
  • 如果有一个客户端发起连接请求,则执行一次,如果有多个客户端发起连接请求,则执行多次

处理请求

java 复制代码
 private void processConnection(Socket clientSocket) {

 }
  • 使用方法专门处理一次连接,在一次连接中可能会涉及多次请求响应交互

如何处理请求和返回响应?

由于TCP面向字节流,我们可以字节流传输的类 InputStream和OutputStream

java 复制代码
   try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){


            } catch (IOException e) {
                throw new RuntimeException(e);
            }
  • 使用try-with-resources管理InputStream和OutputStream,确保流自动关闭。
  • InputStream从网卡中读取数据
  • OutputStream从网卡中写入数据

1)接收请求并解析

java 复制代码
        Scanner scanner = new Scanner(inputStream);
        if(!scanner.hasNext()){
        System.out.printf("[客户端ip:%s,端口号:%d],客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
        }

        String request = scanner.next();
  • 客户端和服务器双方都有自己的缓冲区
  • 客户端发送数据,会先将数据放入服务器缓冲区中
  • 如果服务器缓冲区中没有数据,hasNext()则会陷入阻塞等待中
  • 如果客户端退出,则会触发四次挥手断开连接,服务器会感知到,就会在hasNext()返回false。

2)根据请求计算响应

回显服务器:不会处理数据,输入什么就会返回什么

java 复制代码
            String response = process(request);

使用process方法来实现回显功能

java 复制代码
    public  String process(String request) {
        return request;
    }

如果想要实现特定的功能,直接在process中实现即可

3)返回响应

Scanner的写操作无法自己完成,只能进行读取操作,写操作需要依靠其他的类**(PrintWriter)**

java 复制代码
                PrintWriter printWriter = new PrintWriter(outputStream);
//                将数据写入数据的缓冲区中
                printWriter.println(respond);

冲刷缓冲区

由于缓冲区的特殊机制,缓冲区只有满的时候,才会被发送出去

java 复制代码
                printWriter.flush();
  • 我们这里要保证实时性,客户端每发送一次请求,服务器都要第一时间响应
  • IO操作比较低效,如果每进行一次IO,就要冲刷一次,效率很低,为了让这种低效的操作少一点,等缓冲区满了,才会冲刷

服务器总代码:

java 复制代码
import java.io.*;
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;
    TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //从缓冲区内取出并同意链接
            //将取出的数据使用clientSocket另外保存起来,
            //每有一个客户端,就会出现一个clientSocket对象,所有使用完,必须关闭
            Socket clientSocket = serverSocket.accept();
            //进行数据分析

/*            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();*/
//            这样写开销大,会有很多次的创建和销毁,改进使用线程池

            ExecutorService service = Executors.newFixedThreadPool(3);
            service.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

    //使用这个方法专门处理一次连接,在一次连接中可能会涉及多次请求交互
    private void processConnection(Socket clientSocket) {
        System.out.printf("[客户端ip:%s,端口号:%d],客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        //循环处理请求并返回响应(请求可能不止一次)
        //从网卡中读数据和写数据
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            while (true){
//                byte[] buffer = new byte[1024];
//                int n = inputStream.read(buffer);
//                //将字节数组转换为字符串
//                if(n==-1){
//                    System.out.printf("[客户端ip:%s,端口号:%d],客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
//                    break;
//                }
//                String request = new String(buffer,0,n);
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[客户端ip:%s,端口号:%d],客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
//                1.接受请求并解析
                //客户端必须有一个空格或者换行符
                String request = scanner.next();
//                2.根据请求计算响应
                String respond = process(request);
//                3.返回响应
                //返回的是字节数组类型
//                outputStream.write(request.getBytes(),0,request.getBytes().length);
                //返回字符串类型(各种类型)
                PrintWriter printWriter = new PrintWriter(outputStream);
//                将数据写入数据的缓冲区中
                printWriter.println(respond);
                //冲刷缓冲区
                printWriter.flush();

                //打印日志
                System.out.printf("[客户端ip:%s,端口号:%d],req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort()
                ,request,respond);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                //必须进行close,
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

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

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

注意:

  • 在服务器中,ServerSocket对象不需要被消耗,整个程序中只有一个ServerSocket对象,它的生命周期要伴随整个程序,不能提前关闭,只有程序退出了,才会被释放
  • 方法中的Socket必须要释放,每出现一个客户端,就会随之出现一个Socket对象,如果不释放,Socket对象会越来越多,将文件描述符表占满(内存泄露问题)

2. 客户端

构造方法

java 复制代码
    TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }
  • 这里不需要将serverIP和serverPort在类中保存
  • 因为tcp有链接,socket会保存好这两个值

客户端如何发送请求和接收响应?

客户端同样使用字节流进行传输

java 复制代码
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
   
  • 使用try-with-resources管理InputStream和OutputStream,确保流自动关闭。
  • InputStream从网卡中读取数据
  • OutputStream从网卡中写入数据

1)从控制台读取请求

java 复制代码
            //客户端输入的数据
            Scanner scannerConsole = new Scanner(System.in);

            while(true){
                System.out.print("->");
                //客户端没有输入
                if(!scannerConsole.hasNext()){
                    break;
                }
//              从控制台读取请求
                String request = scannerConsole.next();
            }
  • 使用Scanner进行输入,如果没有输入数据,hasNext()会进入阻塞等待

2)将请求发送给服务器

Scanner只会读取数据,发送使用类PrintWriter

java 复制代码
          PrintWriter writer = new PrintWriter(outputStream);
          writer.println(request);               
  • 向服务器发送数据

冲刷缓冲区

java 复制代码
                //冲刷缓冲区
                writer.flush();

将发送的数据,先放入缓冲区中,等待缓冲区满了,才会发送缓冲区中的内容


3)接收服务器返回的响应

java 复制代码
            Scanner scannerNetwork = new Scanner(inputStream);
                String respond = scannerNetwork.next();
  • 服务器发送的数据,先到达客户端的缓冲区,客户端要从缓冲区读出数据
  • 这里使用Scanner进行读出数据,也可以使用read()方法读取

4)将响应数据显示在控制台

java 复制代码
                System.out.println(respond);

将接收到的字符串响应,直接打印出来即可


客户端总代码:

java 复制代码
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;
    TcpEchoClient(String serverIp,int serverPort) throws IOException {
//        这里不需要将serverIP和serverPort在类中保存
//        因为tcp有链接,socket会保存好这两个值
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            //客户端输入的数据
            Scanner scannerConsole = new Scanner(System.in);
            //通过网络读取
            Scanner scannerNetwork = new Scanner(inputStream);
            //像服务器发送请求
            PrintWriter writer = new PrintWriter(outputStream);
            while(true){
                System.out.print("->");
                //客户端没有输入
                if(!scannerConsole.hasNext()){
                    break;
                }
//                1. 从控制台读取请求
                String request = scannerConsole.next();
//                2.将请求发送给服务端
                writer.println(request);
                //冲刷缓冲区
                writer.flush();
//                3.接受服务端返回的响应
                //从数据缓冲区中读取出内容
                String respond = scannerNetwork.next();
//                4.将响应显示在控制台
                System.out.println(respond);

            }

        } 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();
    }
}

3. 服务端-客户端工作流程

无论是TCP还是UDP都是服务端先启动

创建连接过程

  1. 服务器启动,由于没有客户端建立连接,accept()进入阻塞,等待客户端创建连接
  2. 客户端启动,客户端申请和服务器建立连接
  3. 服务器从accept()阻塞中返回,调用processConnection()方法进行交互

双方交互过程

  1. 服务器进入processConnection()方法,执行到hasNext(),由于客户端没有发送数据,服务器读取不到数据,进入阻塞状态
  2. 客户端在hasNext()这里进入阻塞,等待用户在控制台中输入数据
  3. 用户输入数据,客户端从hasNext()中退出阻塞,将数据发送给服务器,next()阻塞等待服务器返回数据
  4. 服务器从hasNext()阻塞中返回,读取请求并处理,构造响应,发送给客户端
  5. 客户端读取响应并打印

4. 服务器优化

java 复制代码
            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
  • 每来一个客户端,服务器就需要创建出一个新的线程
  • 每次客户端结束,服务器就需要销毁这个线程

如果客户端比较多,那么服务器就需要频繁的创建和销毁 ,开销大


(1)可以通过引入线程池来避免频繁的创建和销毁

java 复制代码
            ExecutorService service = Executors.newFixedThreadPool(3);
            service.submit(()->{
                processConnection(clientSocket);
            });

如果有的客户端处理的过程很短(网站),也有可能客户端处理的时间会很长

处理时间很短的客户端,分配一个专门的线程,有点浪费,所有引入了IO多路复用技术


(2)IO多路复用技术

IO多路复用技术是操作系统提供的机制。

让一个线程去同时去负责处理多个Socket对象

本质在于这些Socket对象不是同一时刻都需要处理

虽然有多个Socket对象,但是同一时间活跃的Socket对象只是少数(大部分的Socket对象都是在等数据),我们可以在等的过程中,去处理活跃的Socket对象


点赞的宝子今晚自动触发「躺赢锦鲤」buff!

相关推荐
rock3653372 小时前
PHP怎样连接MySQL数据库?
数据库·mysql·php
YGGP3 小时前
【每日八股】复习计算机网络 Day1:TCP 的头部结构 + TCP 确保可靠传输 + TCP 的三次握手
网络·tcp/ip·计算机网络
ybdesire4 小时前
Jinja2模板引擎SSTI漏洞
网络·人工智能·安全·web安全·大模型·漏洞·大模型安全
屎到临头想搅便4 小时前
OSPF综合实验(HCIP)
网络·智能路由器
6v6-博客5 小时前
2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?
java·开发语言·php
EasyDSS7 小时前
视频监控EasyCVR视频汇聚平台接入海康监控摄像头如何配置http监听功能?
大数据·网络·网络协议·音视频
九州ip动态7 小时前
避免IP地址关联,多个手机设备的完美公网IP问题
网络协议·tcp/ip·智能手机
跨境卫士-小汪8 小时前
亚马逊热销变维权?5步搭建跨境产品的安全防火墙
网络·经验分享·安全
DanmF--8 小时前
详解与HTTP服务器相关操作
服务器·网络·网络协议·http·unity·c#
小诸葛的博客9 小时前
Docker Overlay 网络的核心工作(以跨节点容器通信为例)
网络·docker·容器