基于系统调用的Linux网络编程——UDP与TCP

基于系统调用实现的Linux网络编程------UDP与TCP

传输层的两种常用协议

  1. TCP协议,其特点是:
    • 传输层协议
    • 有链接
    • 可靠传输
    • 面向字节流
  2. UDP协议,其特点是:
    • 传输层协议
    • 无链接
    • 不可靠传输
    • 面向数据报

套接字socket与网络字节序(大端)

  1. 进行网络传输必须要有对方的ip地址与端口号,于此同时,还会提到"套接字"这个概念,事实上,在进行网络编程时,需要先创建套接字与sockaddr_in结构体,将这两个变量正确初始化后(此步骤需重点注意),再使用bind函数将结构体绑定到套接字上,而套接字的本质其实是文件描述符(不过通过同一个套接字既可以进行读,也可以进行写操作),所谓的使用bind将结构体绑定其实就是将结构体中的信息写到套接字文件描述符指向的内核网络相关数据结构中
  2. 目前,套接字常使用POSIX标准,而事实上,套接字会根据用途分为多种,如网络套接字(后续网络学习的重点),本地套接字(本地通信)等,而为了支持套接字的不同用途,系统提供了下图的三种结构体:

    其中scokaddr_in结构体为网络通信的套接字结构体,而scokaddr_un结构体为本地通信的套接字结构体,至于sockaddr结构体,可以当作另外两种套接字的基类,用于实现套接字接口统一-》按道理是可以使用void*指针来实现的,但其实这三种结构体设计时,C语言还未诞生void*语法
    注:sockaddr_in结构体(位于<netinet/in.h>)的结构如下:
  3. 我们都知道,有的机器采取的是大端字节序,也有的机器采取的是小端字节序,但进行网络通信时,必须要想办法确定对方的信息字节序,才能正确进行信息解析,而为解决该问题,网络中规定,传入网络中的数据必须采取大端字节序,也就是说,所谓的网络字节序其实就是大端字节序(注:小端字节序为小权值放到小地址处,即小小小;而大端即为将小权值放到大地址处,即大小大)

    4. 平时见到的ip地址采取了点分十进制表示,如192.168.1.1,而编程中应使用其代表的4字节整数,即.分割字节,因此,192.168.1.1的ip地址,其4字节整数为:192 168 1 1,其16进制表示为:C0 A8 01 01,其32位无符号整数为:3232235521,完成字符串转整数 + 整数转网络字节序常使用inet_pton函数,下面会具体进行介绍(注:也可使用inet_addr函数,但不推荐,该函数已过时)

UDP协议常用系统调用接口

编程过程中常会使用<sys/types.h>, <sys/socket.h>, <netinet/in.h>, <arpa/inet.h>这四个头文件,下面的函数均是在这四个头文件中定义的(某一个或某两个......),就不再一一介绍函数所在文件了,并且,对于网络字节序的转换,仅需要对ip与端口号进行转换,但无需将收发的信息进行转换,收发信息的转换会由内核自动完成

  1. 关于字节序转换的四个函数uint32_t htonl(uint32_t hostlong);用于将32位无符号整数从主机字节序转换成网络字节序,而uint32_t ntohl(uint32_t netlong);用于将32位无符号整数从网络字节序转换成主机字节序,以及uint16_t htons(uint16_t hostshort);与uint16_t ntohs(uint16_t netshort);函数,即为16位的的转换函数
  2. int socket(int domain, int type, int protocol);,功能为创建套接字(socket文件描述符),第一个参数表示域的概念,表示是做本地通信还是网络通信(网络通信时使用AF_INET),第二个参数表示进行通信的套接字类型,使用SOCK_DGRAM时表示无链接,大小固定报文(即使用UDP通信时采取该宏),第三个参数代表要设定的协议类型,当前两个参数为AF_INET + SOCK_DGRAM时已经证明是UDP了,设置为0默认即可,返回值为一个文件描述符,失败返回-1并置错误码
  3. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,功能为将套接字绑定到地址,即将套接字与sockaddr_in结构体绑定(上述小标题中已提到),第一个参数为要绑定的套接字,第二个参数为上面提到的实现多态的结构体(需要将两个派生类指针强转为基类指针进行传参),第三个参数为第二个参数传递的结构体的大小,返回值为0成功,返回-1失败,失败时会置错误码,同时,更需要注意的是,创建派生类结构体后,需要先将结构体使用memset/C++{}等方式进行全零初始化,在使用UDP传输时,需要将其中的local.sin_family(协议家族)置为AF_INET,local.sin_port置为htons(_port)(未来需将端口号传入网络,因此需使用htons函数将_port端口号转换为网络序列(大端)),local.sin_addr.s_addr置为inet_addr(_ip.c_str())(1.将点分十进制string的ip转换为4字节整数,2.将该4字节转换为网络端(大端)->两需求可使用inet_addr函数实现,但现代编程中更推荐使用线程安全的inet_pton函数),但对于对于服务端,常建议将ip设置为INADDR_ANY宏(0),可用于接收全部发送到本机ip的端口匹配的消息,而无需手动指定ip
  4. int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);,功能为接收数据,第一个参数为套接字,第二个参数为输出型参数,即接收数据的缓冲区,第三个参数为缓冲区可使用大小,第四个参数为标志位,第五个参数为输出型参数,应传递用于接收发送端的sockaddr_in结构体信息的结构体指针,第六个参数为输入 + 输出型参数,输入时表示第五个参数的结构体大小,输出为写入第五个参数的字节数(网络编程中输入输出大小一致),返回值为成功接收的字节数,失败返回-1并置错误码
  5. int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);,功能为将数据发送到指定地址,第一个参数为发送端的套接字,第二个参数为要发送的数据,第三个参数为要发送的数据长度,第四个参数为标志位(通常置为0表默认),第五个参数为接收方的sockaddr_in结构体指针,第六个参数为指定地址的长度,返回值为成功发送的字节数,失败返回-1并置错误码
  6. int inet_pton(int af, const char *src, void *dst);功能为将点分十进制字符串形式的IP地址转换为网络字节序的二进制格式,第一个参数表示地址族,AF_INET表示IPv4地址,AF_INET6表示IPv6地址,第二个参数为输入参数,即点分十进制字符串形式的IP地址(如"192.168.1.1"),第三个参数为输出参数,转换结果存放的缓冲区,当af为AF_INET时,dst应为struct in_addr\*类型(struct sockaddr_in的成员之一),当af为AF_INET6时,dst应为struct in6_addr*类型,返回1表示转换成功;返回0表示src不是有效的网络地址格式;返回-1表示af参数无效(不是AF_INET或AF_INET6),errno被设置
  7. const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);功能为将网络字节序的二进制格式IP地址转换为点分十进制字符串形式,第一个参数表示地址族,AF_INET表示IPv4地址,AF_INET6表示IPv6地址,第二个参数为输入参数,指向包含网络字节序IP地址的缓冲区,当af为AF_INET时,dst应为struct in_addr\*类型(struct sockaddr_in的成员之一),当af为AF_INET6时,src应为const struct in6_addr类型,第三个参数为输出参数,转换结果存放的字符串缓冲区,第四个参数为输出缓冲区的大小,当af为AF_INET时,size应至少为INET_ADDRSTRLEN(16),当af为AF_INET6时,size应至少为INET6_ADDRSTRLEN(46),返回值:成功时返回指向dst的指针,失败时返回NULL,errno被设置
  8. int close(int fd);,功能为关闭文件描述符,返回值为0成功,返回-1失败并置错误码,这是老朋友了,没错,关闭网络套接字时也需要使用位于<unistd.h>头文件中的该函数

UDP客户端,服务端网络编程注意事项

  1. 对于客户端的创建,一般不建议使用自定义端口号,原因是自定义时,可能会使用到系统已使用过的端口号,导致发生冲突,而当首次发送消息时,若操作系统检测到进程未绑定端口,则会进行隐式绑定,此方式也可用于端口号的冲突避免
  2. 服务端创建时,建议将ip设置为INADDR_ANY宏(0),可用于接收全部发送到本机ip的端口匹配的消息,而不建议手动指定ip
  3. 使用云服务器进行网络编程时,会发现无法直接使用公网ip作为进程的网络ip,原因是云服务器厂商进行了相关处理,这里不进行深入研究,可使用内网ip进行连接,但还是最推荐使用上述2点中的INADDR_ANY宏
  4. 经由云服务器厂商的处理,当使用同台机器启动客户端和服务端时,其中一端不能使用公网ip作为接收端为另一端发送数据,会接收不到,而若不再同台机器,则可以使用公网ip(基本也只能使用公网ip)(本质是NAT回环问题)
  5. 协议家族中AF_INET宏与PF_INET宏等价
  6. 使用netstat -anup命令可以查询端口占用情况,其中-a参数表示显示所有端口,-n参数表示显示数字,-u参数表示显示UDP端口,-p参数表示显示进程信息
  7. ifconfig命令可用于查看所有网络接口(网卡)的 IP 地址、MAC 地址、状态等信息
  8. recvfrom函数若成功调用,则会覆盖式重写第五和第六个参数,也就是说,虽然建议将第五个参数在使用前进行{}初始化,但没必要再为其中元素赋值
  9. 关于sendto是按照buffer的第一个\0停止发送还是直接发送第三个参数个数据,测试结果是会直接发送第三个参数个数据,这点要多加注意
  10. sockaddr_in这类"网络结构体",其成员必须始终保持网络字节序,不要乱改

TCP协议常用系统调用接口

首先,TCP协议代码的书写也需要进行套接字创建以及创建sockaddr_in结构体进行bind操作,但与UDP不同的是,TCP协议是面向连接的,这也就意味着,需要使用一些函数进行连接操作,另外,由于TCP是面向字节流的,所以不能使用UDP的recvfrom以及sendto进行通信,而应使用read以及write,下面来介绍TCP协议常用系统调用接口

  1. int socket(int domain, int type, int protocol);,功能为创建套接字,返回值为创建套接字的文件描述符,失败返回-1并置错误码,创建TCP套接字时第一个参数传递AF_INET宏,第二个参数传递SOCK_STREAM宏,第三个参数传递0即可
  2. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,功能为将套接字绑定到指定地址,返回值为0成功,返回-1失败并置错误码
  3. int listen(int sockfd, int backlog);,功能为将套接字转为监听状态,返回值为0成功,返回-1失败并置错误码
  4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);,功能为接受连接,若为收到其他客户端的连接请求则阻塞,收到则返回值为创建套接字的文件描述符,失败返回-1并置错误码(失败的情况之一是listenSockfd套接字失效)
  5. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,功能为将套接字转为连接状态,返回值为0成功,返回-1失败并置错误码
  6. ssize_t read(int fd, void *buf, size_t count);,功能为从文件描述符fd中读取数据,返回值为成功读取的字节数,失败返回-1并置错误码
  7. ssize_t write(int fd, const void *buf, size_t count);,功能为将数据写入文件描述符fd中,返回值为成功写入的字节数,失败返回-1并置错误码
  8. int close(int fd);,功能为关闭文件描述符,返回值为0成功,返回-1失败并置错误码
  9. int shutdown(int sockfd, int how);,功能为关闭套接字的读写功能,返回值为0成功,返回-1失败并置错误码

TCP服务端与客户端创建流程

服务端创建流程

  1. 调用socket函数创建listenSockfd套接字用于监听
  2. 创建sockaddr_in结构体并进行初始化(初始化流程与UDP结构体初始化一致:全零初始化 + 协议家族AF_INET + sin_port设为网络字节序port + sin_addr.s_addr = INADDR_ANY),并进行bind操作(bind操作也与UDP一致)
  3. 通过listenSockfd套接字调用listen函数,即可监听连接该服务端的客户端(注:listen调用后服务端就可被客户端连接了)
  4. 调用accept函数从内核中获取能与监听到的申请连接当前服务端的客户端通信的套接字sockfd
  5. 通过accept函数得到的sockfd套接字调用read函数进行读取操作
  6. 调用write函数将数据写入客户端套接字中
  7. 调用close函数关闭套接字

客户端创建流程

  1. 调用socket函数创建sockfd套接字
  2. 调用connect函数将sockfd套接字转为连接状态,同时进行隐式bind
  3. 调用write函数将数据写入服务端套接字中
  4. 调用read函数进行读取操作
  5. 调用close函数关闭套接字

序列化与反序列化

原因

网络传输时,无论UDP还是TCP,都会面临一个问题:将数据传送到另一个平台时,接收方可能是其他操作系统,或使用其他语言进行接收,因此,若比如尝试将C的结构体作为字节流直接发送给对端处,就可能因为操作系统不同导致内存对齐的方案不同,并且其他语言诸如python就算正确收到了字节流,由于python中没有C的结构体类型,所以也无法将其转换为C发送方想传达的数据,因此,就需要将要发送的数据进行序列化以及反序列化来确保双方收到数的同时能够正确解析,但值得一提的是,在单个操作系统内部(同台机器)的协议实现,全部都是传递结构体二进制序列,原因是,操作系统都是C写的,且由于是同一个操作系统内部,也就不存在内存对齐问题

怎么做以及如何做

那么,该如何进行序列化以及反序列化呢?答案是,序列化是将"内存中的数据结构"转换为"可传输/可存储的字节序列",而常见方案就是将要发送的数据转换为字符串类型,原因是,大多数主流语言都支持字符串类型,而反序列化则是将字符串重新转换回发送端想表达的数据,而为了快速,清晰,正确的进行反序列化,就推荐使用json串等格式方案,举例来说,就是以下字符串结构:

cpp 复制代码
"{
   "age" : 20,
   "name" : "\u5c0f\u8463",
   "sex" : "\u7537"
}"

即,JSON串的格式为:使用:分隔键值,使用{},组织结构,并添加适量空白符用于可读性,而序列化为json串以及将json串反序列化的工作可通过jsoncpp/json/json.h第三方库完成

具体来说,实现转换工作的代码如下:

cpp 复制代码
// 0.创建Json::Value对象
    Json::Value root;

    // 1.像对象中插入key : val
    root["name"] = "小董";
    root["sex"] = "男";
    root["age"] = 20;

    // 2.将root中的数据提取为Json字符串(方案一:使用toStyledString函数)(原json格式)
    std::string infor = root.toStyledString();// toStyledString函数可用于提取字符串
    std::cout << infor << "\n";

    // 2.将root中的数据提取为Json字符串(方案二:使用FastWriter类的write方法提取字符串)(去除\n的格式)
    Json::FastWriter writer;
    infor = writer.write(root);
    std::cout << infor << "\n";

    // 2.将root中的数据提取为Json字符串(方案三:使用StyleWriter类的write方法提取字符串)(原json格式)
    Json::StyledWriter swriter;
    infor = swriter.write(root);
    std::cout << infor << "\n";

    // 2.将root中的数据提取为Json字符串(方案四:使用StreamWriterBuilder类的newStreamWriter方法创建StreamWriter对象,使用其write函数将json中的数据存储如ss)(原json格式)
    Json::StreamWriterBuilder sbuilder;
    std::unique_ptr<Json::StreamWriter> streamWriter(sbuilder.newStreamWriter());
    std::stringstream ss;
    int n = streamWriter->write(root, &ss);// 返回值为负数即转换失败
    if(n < 0)
    {
        exit(1);
    }
    std::cout << ss.str() << "\n";


    // 3.解决汉字编码问题(asString函数)
    std::string name = root["name"].asString();// asString具备自动处理Unicode转义的功能,可以将Json中原本被转换为Unicode编码的中文重新转回中文
    std::cout << name << "\n";
    std::string sex = root["sex"].asString();// asString具备自动处理Unicode转义的功能,可以将Json中原本被转换为Unicode编码的中文重新转回中文
    std::cout << sex << "\n";

    // 4.反序列化(将json串转回数据)
    Json::Reader reader;
    bool ok = reader.parse(infor, root);// 将infor中的json串数据转换到了root对象中,然后使用as系列函数即可转为元数据
    if(!ok)
    {
        exit(1);
    }
    name = root["name"].asString();
    sex = root["sex"].asString();
    int age = root["age"].asInt();// bool,char等也是整形,需调用asInt函数进行转换
    std::cout << name << "\n";
    std::cout << sex << "\n";
    std::cout << age << "\n";

协议

协议的作用

由于TCP传输数据是面向字节流的,所以单次read/write单次收发的报文可能是不完整的,也可能单次收发了多个,当报文完整性不确定时,大概率无法正确进行反序列化操作,因此,需要使用"协议"来确保报文完整性

注:UDP是面向报文的,每次收发(recvfrom/sendto)均可保证处理了一个完整报文,所以j简单场景无需使用协议进行完整性保证,但由于UDP不保证可靠、有序、不重复,因此在应用场景中仍需要自定义协议来处理完整性、顺序和校验等问题

何为协议

协议其实就是一种约定,在Linux中往往通过结构体体现,举例来说,下图就是Linux中UDP协议报头的实现:

也就是说,协议可以自定义,也可以使用广为人知的方案(如http/https协议)

实现协议

这里先使用简单的自定义协议来举例:假设要实现一个简单的网络版本计算器,那就需要C端向S端发送三个参数数据,即数a,数b,运算符,那么,这里规定,使用json串进行数据的传输,且每组数据的格式为json串长度len + \r\n + json串 + \r\n,如此规定,便可根据\r\n作为分隔符正确读取到json串长度len,而len又可以确保json的完整性,具体实现可参考gitee:https://gitee.com/xiao-dongs-code-repository/gitee_dmk/blob/master/Linux/2026_4_17/protocol.hpp

UDP与TCP简单项目代码示例(UDP实现简单网络聊天室,TCP实现网络字典/远程命令行)

  1. UDP:https://gitee.com/xiao-dongs-code-repository/gitee_dmk/tree/master/Linux/2026_4_12
  2. TCP:https://gitee.com/xiao-dongs-code-repository/gitee_dmk/tree/master/Linux/2026_4_14
相关推荐
小小de风呀2 小时前
de风——【从零开始学C++】(五):内存管理
开发语言·c++
数智化精益手记局2 小时前
什么是设备维护管理?设备维护管理包含哪些内容?
大数据·网络·人工智能·安全·信息可视化
charlie1145141914 小时前
嵌入式Linux驱动开发——新 API 字符设备驱动完整教程 - 从设备结构体到应用测试
linux·运维·驱动开发
CHANG_THE_WORLD4 小时前
C语言中的 %*s 和 %.*s 和C++的字符串格式化输出
c语言·c++·c#
消失的旧时光-19434 小时前
C语言对象模型系列(四)《Linux 内核里的 container_of 到底是什么黑魔法?》—— 一篇讲透 Linux 内核的“对象模型”核心技巧
linux·c语言·算法
SWAGGY..4 小时前
Linux系统编程:(二)基础指令详解
linux·运维·服务器
kdxiaojie5 小时前
U-Boot分析【学习笔记】(3)
linux·笔记·学习
螺丝钉的扭矩一瞬间产生高能蛋白5 小时前
QT的C++接口基础用法
c++·qt·嵌入式软件·嵌入式linux·linux应用
烛衔溟5 小时前
TypeScript 接口继承与混合类型
linux·ubuntu·typescript