Linux | 套接字(socket)编程 | TCP协议讲解 | 通信模型搭建

news2024/12/30 3:27:12

文章目录

    • TCP模型的特性
    • TCP接口介绍
    • TCP服务器套接字设置
    • TCP客户端套接字设置

TCP模型的特性

TCP是属于传输层协议的一种,上篇博客介绍了另一种传输层协议——UDP,关于它们之间的区别,这里再提一下

TCPUDP
传输层协议传输层协议
有连接无连接
可靠连接不可靠连接
面向字节流面向数据报

其中更详细的区别在UDP套接字编程中已经讲解,这里不再赘述,直接进入TCP相关接口的介绍

TCP接口介绍

在这里插入图片描述
在这里插入图片描述
listen:使socket文件处于监听状态,此时网络中的设备可以向处于监听状态的套接字文件发送连接请求,backlog则是套接字文件的监听队列的长度。因为TCP协议是面向连接的协议,在通信之前需要双方确定连接,所以服务端通常需要先进入监听状态等待其他设备的连接,建立了连接才能进行通信。
在这里插入图片描述
accept:刚才的listen使一个socket文件处于监听状态,accept的作用就是接受socket的监听队列中的第一个套接字文件的连接,并且会创建一个服务套接字,服务套接字的套接字类型与协议家族和监听套接字的相同(端口号肯定不同),服务器用socket监听套接字监听网络中的连接请求,用accept返回的套接字文件为请求套接字提供服务。两个套接字文件都用来通信,一个是监听套接字,以通信的方式监听网络中的请求,一个是服务套接字,以通信的方式为发送请求的客户端提供服务。

参数解释:
socket:监听套接字的文件描述符
address:输出型参数,接受连接的套接字信息结构体
address_len:输出型参数,套接字信息的结构体的字节大小

所以accept可以接受监听队列中的连接请求,并以输出型参数的形式保存请求方的套接字信息,最后accept会返回一个套接字文件fd,用来为请求方提供服务。因此,只要accept了一次请求,就会有一个套接字文件被创建以提供服务,如果请求很多,用来提供服务的套接字相应的也会有很多,但是一台设备上的文件数是有上限的,如果对请求方提供的服务完成,没有及时关闭提供服务的套接字文件,就会造成文件资源的泄漏,最终造成服务器的崩溃,所以在编写TCP通信的代码时,一定要记住:当服务完成,用于提供服务的套接字文件必须及时关闭。
在这里插入图片描述
如果accept调用失败,函数会返回-1并设置错误码
在这里插入图片描述
补充两个IP地址转换的接口,一个是inet_aton,将字符串形式的IP转换成一个32位无符号整数

cp:是要转换字符串IP的起始地址
inp:是转换后的IP存储的地址

可以看到转换后的inp也是一个地址,并且其类型是struct in_addr*在这里插入图片描述
这类型不就对应了struct sockaddr_in的成员struct in_addr吗?原本需要直接对struct in_addr的成员in_addr_t类型的s_addr直接赋值,但是使用inet_aton函数就不需要了,该函数直接返回一个类型为struct in_addr的结构体,里面包含了以32位无符号整数存储的IP地址在这里插入图片描述

struct sockaddr_in local;
_ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);

(INADDR_ANY表示绑定该主机上的所有ip地址,只要网络中的设备发送的ip地址是该主机的其中一个,该主机就可以接受到)

还有一个接口inet_ntoa,可以将以32位整数形式表示的ip地址转换成点分十进制字符串形式的ip地址在这里插入图片描述
inet_ntoa的形参有点特殊,不是一个32位整数,而是一个结构体struct in_addr,该结构体是struct sockaddr_in结构体下的一个成员sin_addr的类型

struct sockaddr_in local;
char ip[] = inet_ntoa(local.sin_addr);
// ip数组就是以点分十进制表示的ip地址

在这里插入图片描述
connect:连接到一个socket文件,通常用于客户端与服务器的连接

socket:自己的socket文件fd,用自己的socket文件与服务器的socket文件进行通信
address:需要连接的套接字文件信息结构体
address_len:address结构体的字节大小

也就是说客户端使用connect与服务器连接,但是需要先知道服务器的套接字信息,创建了自己的套接字,有了服务器的套接字信息,就可以与服务器进行连接了

TCP服务器套接字设置

// util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/stat.h>

#define SOCK_FAIL 1
#define BIND_FAIL 2
#define LSTE_FAIL 3
#define CONN_FAIL 4
#define USAG_ERRO 5
#include "util.hpp"
#include <signal.h>

class tcpServer
{
public:
    tcpServer(uint16_t port, std::string ip = "") : _ip(ip), _port(port) {}
    ~tcpServer() {}

    void init()
    {
        // 创建套接字文件
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "socket: fail" << std::endl;
            exit(SOCK_FAIL);
        }
        // 填充套接字信息
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        _ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);
        // 将信息绑定到套接字文件中
        if (bind(_listen_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind: fail" << std::endl;
            exit(BIND_FAIL);
        }
        // 至此,套接字创建完成,所有的步骤与udp通信一样
        // 使套接字进入监听状态
        if (listen(_listen_sockfd, 5) < 0)
        {
            std::cerr << "listen: fail" << std::endl;
            exit(LSTE_FAIL);
        }
        // 套接字初始化完成
        std::cout << "listen done" << std::endl;
    }

    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // 设置SIGCHLD信号为忽略,子进程会自动释放资源
        // 创建保存套接字信息的结构体
        struct sockaddr_in peer;
        socklen_t peer_len = sizeof(peer);
        // 接受监听队列中的套接字请求
        while (1)
        {
            int server_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &peer_len);
            if (server_sockfd < 0)
            {
                std::cerr << "accept: fail" << std::endl;
                continue;
            }
            std::cout << "accept done" << std::endl;
            // 提取请求方的套接字信息
            uint16_t peer_port = ntohs(peer.sin_port);
            std::string peer_ip = inet_ntoa(peer.sin_addr);
            // 打印请求方的套接字信息
            std::cout << "accept: " << peer_ip << " [" << peer_port << "]" << std::endl;
            // 提供服务
            pid_t id = fork();
            if (id == 0)
            {
                // child
                service(server_sockfd);
            }
        }
    }

    void service(int server_sockfd)
    {
        char in_buffer[1024];
        while (1)
        {
            ssize_t ret = read(server_sockfd, in_buffer, sizeof(in_buffer));
            if (ret > 0)
            {
                in_buffer[ret] = '\0';

                if (strcasecmp("quit", in_buffer) == 0)
                {
                    std::cout << "client quit, service done" << std::endl;
                    break;
                }
                // 假设服务是在client发送的字符串后添加一串字符串
                strcat(in_buffer, ",service done");
                // 服务完成,将字符串发送给client
                write(server_sockfd, in_buffer, sizeof(in_buffer));
            }
            else if (ret == 0)
            {
                std::cout << "client quit, service done" << std::endl;
                break;
            }
            else
            {
                std::cout << "service fail" << std::endl;
                break;
            }
        }
        close(server_sockfd);
    }

private:
    std::string _ip;
    uint16_t _port;
    int _listen_sockfd;
};

int main()
{
    tcpServer server(8080);
    server.init();
    server.loop();
    return 0;
}

封装tcpServer类,该类的成员_ip保存了服务器的IP地址,_port保存了端口号,以及_listen_sockfd保存了监听套接字的文件描述符。tcpServer的构造就是初始化这些成员,如果用户没有传入指定的IP地址,那么tcpServer的成员_ip就是一个空字符串,在绑定套接字信息时,会将该主机上的所有IP绑定到套接字文件中(INADDR_ANY)。

tcpServer的初始化函数:init,init会调用socket接口,创建一个套接字(打开一个套接字文件),然后填充套接字信息到struct sockaddr_in结构体中,接着调用bind绑定套接字信息到套接字文件中,至此TCP套接字的初始化和UDP通信一样,UDP通信的服务器此时已经初始化完成,可以调用recvfrom接收来自客户端的信息了。这是无连接的UDP通信,而TCP通信是面向连接的,所以在通信之前需要确定双方的连接,具体表现为:服务器调用listen进入监听状态,客户端调用connect连接处于监听状态的服务器。所以tcpServer的init初始化的套接字被用来监听网络中的连接请求,是客户端与服务器之间连接的窗口。因此,init总体分为四步,创建套接字,填充套接字信息,绑定套接字,使套接字处于监听状态,init完成,服务器就处于监听状态,可以接收网络中的连接请求。

服务器初始化后,就需要为客户提供服务,设置loop函数使服务器对外提供服务:先调用accept接收客户端向监听套接字发送的连接请求,由于accept会创建一个新的套接字并返回其文件描述符,所以我们需要接收accept的返回值,拿到新创建的套接字文件,用新的套接字文件为客户端提供服务。因此,在TCP模型中有两个套接字,一个监听套接字,一个服务套接字,监听套接字是连接服务器与客户端的窗口,服务套接字是服务器为客户端提供服务的窗口。只要服务器接受了监听套接字监听到的连接请求,服务器就需要创建一个服务套接字,通过服务套接字为客户端提供服务。但是服务器是一个进程,所以服务器提供服务时只能一对一的提供,服务完成一个客户再服务下一个客户,很显然,这样一个一个的服务效率太低,所以我们可以创建子进程,使子进程通过服务套接字为客户端提供服务。

但是当子进程服务完成,子进程就会退出,此时的子进程处于僵尸状态,父进程需要回收子进程的资源,如果不回收子进程就会引起资源泄漏的问题。所以父进程需要调用waitpid回收子进程,但是父进程又不能阻塞式的等待子进程的退出,阻塞式的等待与服务器一个个的服务客户没有区别,所以父进程就需要非阻塞式的等待子进程,但是父进程就需要记录所有子进程的pid,这样也有点麻烦,最简单的方法就是将SIGCHLD信号的handler设置为SIG_IGN,使子进程在退出时自动释放自己的资源(但这种方法只有在Linux下有效)。

除了创建子进程,使子进程为客户提供服务的做法,我们还可以创建孙子进程,使父进程退出。就是说,现在的服务器是一个祖父进程,服务器创建了一个子进程,接着在这个子进程中再创建一个子进程,那么服务就创建了两个子进程,它们的关系是祖父进程,父进程与子进程,我们可以使父进程退出,那么子进程成为孤儿进程,由1号进程托管,对客户端提供的服务由子进程完成,当子进程退出时,1号进程就会自动释放它的资源,这样它就不会处于僵尸状态,造成资源的泄漏了。但是祖父进程就需要阻塞式的等待回收父进程(当父进程创建完子进程后,父进程就会退出,可以说一瞬间父进程就退出了,祖父进程也不会等待太久),不然父进程就会进入僵尸,又造成资源泄漏。
在这里插入图片描述
除了创建孙子进程这样的操作,我们还能创建多线程,但是我们不能join新线程,因为join会造成进程的阻塞,我们只能将线程分离,使其退出时自动释放资源
在这里插入图片描述

在这里插入图片描述

TCP客户端套接字设置

#include "util.hpp"

void usage(const char *filename)
{
    std::cout << "usage:\n\t"
              << filename << "IP port" << std::endl;
}

volatile bool quit = false;

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAG_ERRO);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 服务器套接字的填充
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 套接字的创建
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket: fail" << std::endl;
        exit(SOCK_FAIL);
    }
    // 与服务器的连接
    if (connect(sockfd, (const struct sockaddr*)&server, sizeof(server)) < 0)
    {
        std::cerr << "connect: fail" << std::endl;
        exit(CONN_FAIL);
    }
    std::cout << "connect done" << std::endl;
    while (!quit)
    {
        std::cout << "Please Enter#";
        char in_buffer[1024] = {0};
        char out_buffer[1024] = {0};
        std::cin.getline(in_buffer, sizeof(in_buffer));
       // std::cout << in_buffer << std::endl;
        if (strcasecmp("quit", in_buffer) == 0)
        {
            // 注意不要退出,让客户端向服务器发送quit,服务器接收quit将关闭服务
            quit = true;
        }

        ssize_t w_ret = write(sockfd, in_buffer, sizeof(in_buffer));
        if (w_ret > 0)
        {
            ssize_t r_ret = read(sockfd, out_buffer, sizeof(out_buffer));
            if (r_ret > 0)
            {
                out_buffer[r_ret] = '\0';
                std::cout << "receive: " << out_buffer << std::endl;
            }
            else 
            {
                std::cerr << "read: fail" << std::endl;
                break;
            }
        }
        else
        {
            std::cerr << "write: fail" << std::endl;
            break;
        }
    }
    return 0;
}

客户端要做的就是:填充服务器的套接字信息,并创建自己的套接字,最重要的就是调用connect用自己的套接字连接服务器的监听套接字,也就是发送与服务器的连接请求,当服务器调用listen处于监听状态时,客户端的连接才能发送成功,由于TCP协议下发送的数据是流式的,所以我们可以使用write和read向套接字文件中读写信息(UDP的数据是数据报式的,recvfrom和sendto是用来发送数据报式数据的接口)

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

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

相关文章

Word控件Spire.Doc 【评论】教程(3):在C#、VB.NET中从Word文档中提取注释并保存在TXT文件中

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

[Leetcode] 合并两个有序数组、链表

1.合并两个有序数组 原地合并数组&#xff0c;即不使用额外的空间 --> 使用三个指针&#xff0c;从尾部往前处理 题目链接&#xff1a;https://leetcode.cn/problems/merge-sorted-array/ nums1 总长度 mn&#xff0c;自身长度m&#xff1b;nums2 自身长度n&#xff0c; 使…

SSRF渗透与攻防(一)

目录 前言 SSRF是什么 危害&#xff08;利用方式): SSRF漏洞原理&#xff1a; CURL协议&#xff1a; SSRF常见场景 社会化分享功能&#xff1a; 如何发现SSRF漏洞 工具利用&#xff1a; 如何防御SSRF漏洞 前言 SSRF(Server-Side Request Forgery:服务器端请求伪造) 是…

rocketmq 实战问题汇总

rocketmq 实战过程会遇到这样或者那样的问题&#xff0c;今天我们专门抽出一篇文章来分析一下汇总一下&#xff0c;避免以后踩同样的坑&#xff1a; 1、找不到JDK的问题&#xff1a; 综合分析&#xff0c;是因为JDK安装的目录有空格导致的&#xff1a;Program Files 两个单词之…

电子招标采购系统源码—企业战略布局下的采购寻源

​ 智慧寻源 多策略、多场景寻源&#xff0c;多种看板让寻源过程全程可监控&#xff0c;根据不同采购场景&#xff0c;采取不同寻源策略&#xff0c; 实现采购寻源线上化管控&#xff1b;同时支持公域和私域寻源。 询价比价 全程线上询比价&#xff0c;信息公开透明&#xff0…

CANoe—基于DoIP通过CAPL实现与ECU通信测试

如下连接是在CANoe中基于DoIP通过加载诊断数据库实现CANoe与待测ECU诊断通信: CANoe链接 本文继续此话题,通过一个简单的CAPL Demo,实现CANoe与ECU进行DoIP通信。 首先在CANoe新建Ethernet工程: 在CANoe “Simulation Setup”中新建CAPL Test Module: 在此例中采用CANo…

DHCP原理和实验

目录 DHCP基本认识和原理 场景一、同网段DHCP 场景二、不同段DHCP&#xff08;中继DHCP&#xff09; DHCP基本认识和原理 DHCP&#xff08;Dynamic Host Configuration Protocol动态主机协议&#xff09;。 作用&#xff1a;为局域网络中主机动态分发地址&#xff0c;以及…

C#里使用ExcelDataReader读取EXCEL文件的简单方法

C#里使用ExcelDataReader读取EXCEL文件的简单方法 读取EXCEL文件是比较常见的需求,所以在C#里也会经常遇到。 比如客户需要保存的条码数据,他们可以使用EXCEL来扫码进去,并且进行修改和核验, 然后软件就需要读取这些EXCEL文件,并且从这里得到所需要的条码。 要从EXCEL里…

Android 查看隐私权限方法调用者集合

背景 辛辛苦苦迭代完当前版本&#xff0c;准备推送 App 到应用市场上架&#xff0c;却收到拒审通知&#xff1a;App隐私合规上架护航版检测报告&#xff0c; 报告内容&#xff1a; 场景2:APP以隐私政策弹窗的形式向用户明示收集使用规则&#xff0c;未经用户同意&#xff0c;…

页面崩溃了!记录一次测试中出现的前端内存溢出现象

前情回顾 前几天在一次web应用测试过程中&#xff0c;前端发起了向后端接口的查询请求&#xff0c;由于后端响应较慢&#xff0c;前端一直处于等待响应返回状态。在几分钟后&#xff0c;突然页面出现让人惊悚的“噢噢&#xff0c;页面崩溃了”几个大字。 看到这几个字的一瞬间…

基于GitLab构建企业级CICD-Gitlab-Runner

背景 在过往企业开发中&#xff0c;大部分企业从开发到测试&#xff0c;到部署目前还是手工进行在一些某些中大型企业中&#xff0c;目前构建及部署还是直接使用二进制包部署&#xff0c;或直接单机运行在某些场合下&#xff0c;仓库中代码的编译需要硬件支持&#xff0c;致使…

SSM框架学习记录-Maven_day01

1.分模块开发 将原始模块按照功能拆分成若干个子模块&#xff0c;方便模块间的相互调用&#xff0c;接口共享&#xff1a;比如有订单和商品两个模块&#xff0c;它们都需要使用到商品的模型类&#xff0c;如果在这两个模块中都写模型类&#xff0c;就会出现重复代码&#xff0c…

Doris部分列更新在广告行业应用

背景&#xff1a;业务需要在不同的时间点对同一个session_id上的广告行为&#xff08;展示、点击、转换等&#xff09;数据的更新。 基于HBase归因 更新原理&#xff1a;以session_id为Key在HBase中写入数据&#xff0c;数据更新是先点查到历史数据&#xff0c;补齐当前数据后…

风已起,待云涌---多维度理解云安全

Fix the Unknown,Before You Know it. 新时代大门开启的时候&#xff0c;蜂拥而上的大都是勇士&#xff0c;风已起&#xff0c;待云涌&#xff01; 1.云安全&#xff1a; 未来安全的能力将成为计算、存储、网络之外的第四大基础设施&#xff0c;并全部融入到云基础设施中&…

Python爬虫入门 ~ selenium访问元素信息与交互基本使用

访问元素信息 前面我们成功定位到了页面的标签元素&#xff0c;那接下来就该轮到获取元素的信息了&#xff0c;常用的函数有以下几种: get_attributetexttag_name 前置准备 from selenium import webdriver from selenium.webdriver.chrome.service import Service from sel…

【Django项目开发】用户认证功能开发(四)

一、JWT Token配置 1、下载djangorestframework-jwt pip install djangorestframework-jwt2、settings.py指定使用的认证类JSONWebTokenAuthentication REST_FRAMEWORK {# 指定使用的认证类# a、在全局指定默认的认证类&#xff08;认证方式&#xff09;DEFAULT_AUTHENTICA…

企业金融App评测系列——微众银行以App构筑企业金融服务新生态,成为企业的随身数字银行

易观分析&#xff1a;近年来&#xff0c;疫情驱动小微企业线上化需求不断提升&#xff0c;经营面临的财力、人力、信息获取的紧迫性进一步提升。为更好发展普惠小微金融的商业银行对公服务&#xff0c;正聚焦更好满足小微企业的数字化需求&#xff0c;加快打造企业手机银行等移…

mybatis xml接收日期格式参数

实现方式一&#xff1a; mapper List<AsLLjgcfVO> selectjgcf(Param("rq")Date rq)mapper.xml <select id"selectjgcf" parameterType"string" resultType"com.bysen.mes.domain.VO.AsLLjgcfVO"> where ${rq} between …

使用openEuler系统 搭建Nginx服务器

文章目录1 Nginx服务器1.1 概述1.2 安装1.3 管理nginx1.3.1 概述1.3.2 前提条件1.3.3 启动服务1.3.4 停止服务1.3.5 重启服务1.3.6 验证服务状态1.4 配置文件说明1.5 管理模块1.5.1 概述1.5.2 加载模块1.6 验证web服务是否搭建成功1 Nginx服务器 1.1 概述 Nginx 是一款轻量级…

【Vue】course_3

13.vue3过渡效果 学习&#xff1a;Vue3过渡效果开发&#xff08;内置组件 、内置组件 &#xff09; Vue 提供了两个内置组件&#xff0c;可以帮助你制作基于状态变化的过渡和动画&#xff1a; <Transition> 会在一个元素或组件进入和离开 DOM 时应用动画。本章节会介绍…