【网络编程】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!

相关推荐
汪汪大队u8 小时前
isis整体知识梳理
网络·智能路由器
洋葱圈儿6668 小时前
第八个实验——浮动路由
运维·服务器·网络
SJLoveIT8 小时前
虚拟机的网络模式
网络
IT葛大侠9 小时前
华为S5720配置telnet远程
网络·华为
Morphlng10 小时前
wstunnel 实现ssh跳板连接
linux·服务器·网络·ssh
星如雨落11 小时前
Linux VScode 安装PHP环境
linux·php·visual studio code
安卓开发者13 小时前
鸿蒙NEXT网络通信实战:使用HTTP协议进行网络请求
网络·http·harmonyos
为java加瓦13 小时前
IO多路复用的两种触发机制:ET和LT触发机制。以及IO操作是异步的还是同步的理解
java·服务器·网络
岑梓铭13 小时前
计算机网络第四章(8)——网络层《ICMB网际控制协议》
网络·计算机网络
宁小法13 小时前
PHP 数组 如何将新元素加到数组第一个位置(支持指定key => value)
php·数组·首个元素