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

news2025/4/23 13:35:19

目录

一. TCP API 

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

1. 服务器

2. 客户端

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

4. 服务器优化


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


一. TCP API 

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


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接听

 代码实现:

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

处理请求

 private void processConnection(Socket clientSocket) {

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

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

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

   try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){


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

1)接收请求并解析

        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)根据请求计算响应

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

            String response = process(request);

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

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

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

3)返回响应

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

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

冲刷缓冲区

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

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

服务器总代码:

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. 客户端

 构造方法

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

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

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

        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){

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

1)从控制台读取请求

            //客户端输入的数据
            Scanner scannerConsole = new Scanner(System.in);

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

2)将请求发送给服务器

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

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

冲刷缓冲区 

                //冲刷缓冲区
                writer.flush();

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


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

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

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

                System.out.println(respond);

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


客户端总代码:


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. 服务器优化

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

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


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

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

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

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


(2)IO多路复用技术

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

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

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

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2340811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从零开始配置 Zabbix 数据库监控:MySQL 实战指南

Zabbix作为一款开源的分布式监控工具,在监控MySQL数据库方面具有显著优势,能够为数据库的稳定运行、性能优化和故障排查提供全面支持。以下是使用Zabbix监控MySQL数据库的配置。 一、安装 Zabbix Agent 和 MySQL 1. 安装 Zabbix Agent services:zabbix…

Java学习手册:RESTful API 设计原则

一、RESTful API 概述 REST(Representational State Transfer)即表述性状态转移,是一种软件架构风格,用于设计网络应用程序。RESTful API 是符合 REST 原则的 Web API,通过使用 HTTP 协议和标准方法(GET、…

读一篇AI论文并理解——通过幻觉诱导优化缓解大型视觉语言模型中的幻觉

目录 论文介绍 标题 作者 Publish Date Time PDF文章下载地址 文章理解分析 📄 中文摘要:《通过幻觉诱导优化缓解大型视觉语言模型中的幻觉》 🧠 论文核心动机 🚀 创新方法:HIO(Hallucination-In…

IOT项目——物联网 GPS

GeoLinker - 物联网 GPS 可视化工具 项目来源制作引导 项目来源 [视频链接] https://youtu.be/vi_cIuxDpcA?sigMaOKv681bAirQF8 想要在任何地方追踪任何东西吗?在本视频中,我们将向您展示如何使用 ESP32 和 Neo-6M GPS 模块构建 GPS 跟踪器——这是一…

Java学习手册:HTTP 协议基础知识

一、HTTP 协议概述 HTTP(HyperText Transfer Protocol)即超文本传输协议,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传输协议。它是一个应用层协议,基于请求-响应模型…

【含文档+PPT+源码】基于微信小程序的健康饮食食谱推荐平台的设计与实现

课程目标: 教你从零开始部署运行项目,学习环境搭建、项目导入及部署,含项目源码、文档、数据库、软件等资料 课程简介: 本课程演示的是一款基于微信小程序的健康饮食食谱推荐平台的设计与实现,主要针对计算机相关专…

Redis 慢查询分析与优化

Redis 慢查询分析与优化 参考书籍 : https://weread.qq.com/web/reader/d5432be0813ab98b6g0133f5kd8232f00235d82c8d161fb2 以下从配置参数、耗时细分、分析工具、优化策略四个维度深入解析 Redis 慢查询问题,结合实战调优建议,帮助开发者…

使用达梦官方管理工具SQLark快速生成数据库ER图并导出

在数据库设计与开发中,实体-关系图(ER 图)作为数据建模的核心工具,能够直观呈现表结构、字段属性及表间关系,是团队沟通和文档维护的重要工具。然而,许多开发者在实际工作中常面临一个痛点:手动…

模型 替罪羊效应

系列文章分享模型,了解更多👉 模型_思维模型目录。转嫁罪责于无辜,维系群体控制与稳定 1 替罪羊效应的应用 1.1 多品牌危机中的行业“背锅侠” 行业背景:食品行业爆发大规模安全危机,多家企业卷入某类食品重金属超标…

TapData × 梦加速计划 | 与 AI 共舞,TapData 携 AI Ready 实时数据平台亮相加速营,企业数据基础设施现代化

在实时跃动的数据节拍中,TapData 与 AI 共舞,踏出智能未来的新一步。 4月10日,由前海产业发展集团、深圳市前海梦工场、斑马星球科创加速平台等联合发起的「梦加速计划下一位独角兽营」正式启航。 本次加速营以“打造下一位独角兽企业”为目…

15.电感特性在EMC设计中的运用

电感特性在EMC设计中的运用 1. 共模电感与差模电感的差异2. 电感的高频等效特性![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b4dc000672af4dd69a528450eb42cf10.png)3. 电感在EMC设计中的使用注意事项3.1 LC滤波计算3.2 并联型多级浪涌防护的电感退耦 1. 共模电感…

uniapp Vue2升级到Vue3,并发布到微信小程序的快捷方法

目录 前言:升级项目的两种方式步骤一、新建项目 【选择-默认模版】二、修改-pages.json三、补充-缺少的文件四、修改-Main.js按照 [官方文档-vue2升级vue3迁移指南](https://uniapp.dcloud.net.cn/tutorial/migration-to-vue3.html) 修改 五、升级-uni-ui扩展组件的…

数据重构如何兼顾效率与性能稳定?zStorage 全闪存分布式存储的技术实践与实测数据

点击蓝字 关注我们 zStorage 作为数据库场景下的全闪存分布式存储,除了性能要好,更重要的是要在各种情况下都能保持“稳定”的好。一个高并发的交易型业务数据库,如果出现轻微的IO抖动,就可能造成数据库并发事务提交的排队&#x…

A2A + MCP:构建实用人工智能系统的超强组合

构建真正有效的连接型人工智能系统的挑战 如果你正在构建人工智能应用,这种情况可能听起来很熟悉: 你需要特定的人工智能能力来解决业务问题。你找到了完成每个单独任务的出色工具。但把所有东西连接在一起却占据了大部分开发时间,还创建了…

力扣每日打卡17 49. 字母异位词分组 (中等)

力扣 49. 字母异位词分组 中等 前言一、题目内容二、解题方法1. 哈希函数2.官方题解2.1 前言2.2 方法一:排序2.2 方法二:计数 前言 这是刷算法题的第十七天,用到的语言是JS 题目:力扣 49. 字母异位词分组 (中等) 一、题目内容 给…

Word处理控件Spire.Doc系列教程:C# 为 Word 文档设置背景颜色或背景图片

在 Word 文档中,白色是默认的背景设置。一般情况下,简洁的白色背景足以满足绝大多数场景的使用需求。但是,如果您需要创建简历、宣传册或其他创意文档,设置独特的背景颜色或图片能够极大地增强文档的视觉冲击力。本文将演示如何使…

掌握 Altium Designer:轻松定制“交换器件”工具栏

在PCB设计过程中,快速交换器件(如电阻、电容、IC等)是提高效率的关键。Altium Designer提供了灵活的工具栏定制功能,让用户可以创建专属的"交换器件"工具栏,将常用操作集中管理,减少菜单切换时间…

【C++ Qt】信号和槽(内配思维导图 图文并茂 通俗易懂)

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论​: 本章是Qt中的第三章,也是我们理解Qt中必备的点 信号槽,它本质由信号和槽两个来实现,其中将细致的讲述如何自定义信号…

从零开始构建微博爬虫:实现自动获取并保存微博内容

从零开始构建微博爬虫:实现自动获取并保存微博内容 前言 在信息爆炸的时代,社交媒体平台已经成为信息传播的重要渠道,其中微博作为中国最大的社交媒体平台之一,包含了大量有价值的信息和数据。对于研究人员、数据分析师或者只是…

三餐四季、灯火阑珊

2025年4月22日,15~28℃,挺好的 待办: 教学技能大赛教案(2025年4月24日,校赛,小组合作,其他成员给力,暂不影响校赛进度,搁置) 教学技能大赛PPT(202…