基于POSIX标准网络编程快速入门及本人学习经验
本文介绍了基于POSIX标准的网络编程入门知识及作者的学习经验。主要内容包括:1)核心概念如套接字、IP地址、端口、协议等;2)TCP/UDP协议特点及适用场景对比;3)网络编程中的字节序问题及转换函数;4)TCP三次握手和四次挥手的通俗解释;5)常用API函数说明。作者分享了学习建议:先掌握网络基础知识再学习API,通过实践和AI辅助解决问题。文章结构清晰,涵盖了网络编程的关键知识点,适合初学者
基于POSIX标准网络编程快速入门及本人学习经验
本人学习经历及经验
这是本人第二次学习POXIS标准网络编程,第一次的时候是简单学了一下,刚开始是看UNIX环境高级编程这本书来学习,后来感觉只跟着书学习效率太低了,就结合视频学习,建议不要直接去学API,因为到时候你可能不理解,会遇到很多很多问题,先去了解一下关于网络通信的基础知识。之后,结合着API然后去学习,比如说使用htons() / htonl()这两个函数的时候,你就要去了解一下什么是大端序什么是小端序,什么是网路字节序什么是主机字节序,要学会去思考,这是我个人感觉比较重要的,然后根据问题去问ai来解决自己的问题,然后真正写案例的时候就会轻而易举。其次就要了解网络通信的基本流程了,熟悉一下TCP和UDP模型。至于写代码中遇到的一些不是很懂的参数,可以去问ai。
1.核心概念
1.1 socket-套接字
1.2 ip地址
1.3 端口
1.4 协议
1.5 四层结构
1.6 字节序
1.6.1 大端序和小端序
1.6.2 网络字节序
1.6.3 注意
1.7 三次握手四次挥手通俗讲解版
2.常用API
2.1 核心套接字函数
2.2 核心数据结构
2.3 数据传输函数
2.4 地址转换函数
3.TCP网络编程模型
3.1 TCP服务器端流程
3.2 TCP客户端流程
4.UDP网络编程模型
4.1 UDP服务端流程
4.2 UDP客户端流程
5.TCP网络编程模型与UDP网络编程模型对比
1.核心概念
1.1Socket (套接字):
网络编程的基石。可以看作是一个文件描述符,但用于网络通信。它是通信端点的抽象,应用程序通过它发送或接收数据包。通俗点来讲就相当于一个插座,如果想和别人通信,就得先建立socket
1.2ip地址:
标识网络中的一台主机,如 192.168.1.1。通俗点讲就是你和别人通信,你得知道人家的地点。
1.3端口 (Port):
标识主机上的一个特定进程(一个 16 位的数字,如 80)。IP 地址 + 端口号唯一确定了互联网上的一个通信端点。
1.4协议
TCP (Transmission Control Protocol): 面向连接的、可靠的、基于字节流的协议。平时某信聊天,用到的就是tcp。
UDP (User Datagram Protocol): 无连接的、不可靠的、基于数据报的协议。像寄明信片,无需连接,不保证顺序和送达,但更高效。大多数情况下,视频电话用的就是udp通信。
任务 | 使用的协议 | 原因 |
---|---|---|
音视频数据流 | 主要用UDP | 追求低延迟和流畅性,可以容忍少量数据丢失。 |
信令和控制 | 主要用到TCP | 需要绝对可靠。比如:1. 拨打/接听/挂断电话的指令2. 加密的密钥交换3. 协商使用哪些编解码器这些指令绝不能丢失或出错,否则通话无法正常建立或结束。 |
文字聊天 | 通常用TCP | 保证每条消息不丢失、不乱序。 |
文件传输 | 通常用TCP | 保证文件完整无误地传输。 |
特性 | TCP (像打电话) | UDP (像现场直播) |
---|---|---|
核心目标 | 可靠性 > 速度 | 速度/实时性 > 可靠性 |
数据丢失 | 重传,直到成功 | 直接丢弃,继续发新的 |
效果 | 卡住、等待、然后继续 | 花屏、模糊、但持续流畅 |
适用场景 | 网页、邮件、文件、通话信令 | 视频/音频流、在线游戏、直播 |
总而言之:TCP更可靠但是速度慢,UDP不是很可靠但是速度快
1.5四层结构
四层结构 |
---|
应用层 |
运输层 |
网络层 |
数据链路层 |
1.6字节序
1.6.1大端序和小端序
如果数据类型占用的内存空间大于1字节,CPU把数据存放在内存中的方式有两种:
大端序:低位字节放在高位,高位字节存在低位
小端序:低位字节放在低位,高位字节放在高位
操作文件的本质就是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件(socket也是文件描述符)
字节序不同的计算机之间传送数据可能出现问题
1.6.2网络字节序
为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序
c语言提供了四个库函数,用于在主机字节序和网络字节序之间转换
网络编程中,数据收发的时候有自动转换机制,不需要程序员进行手动转换,只有向sockaddr_in结构体成员变量填充数据时,才需要考虑字节序的问题
1.6.3注意
✅ 发送数据前:htons() / htonl()(主机→网络)。
✅ 接收数据后:ntohs() / ntohl()(网络→主机)。
✅ 网络数据必须是大端序
🚀 主机字节序一般是小端序,也有可能是大端序。网络字节序由标准规定为大端序
🚀 这样就能保证跨平台、跨网络的正确通信!
1.7 三次握手四次挥手(基于posix标准通俗讲解)
目的
三次握手:在客户端(Client)和服务器(Server)之间建立可靠的TCP连接.
四次分手:TCP连接是数据双向传输,因此每个方向必须单独关闭。四次挥手用于安全、可靠地终止连接。
三次握手
第一次握手:
客户端发起连接请求(connect()),就像打电话一样:“喂,你好,我想和你建立连接”
第二次握手:
服务端回应连接请求(accept()),服务器端回应:“ok”,此时accept函数还在阻塞等待客户端确认
第三次握手:
客户确认,connect()函数阻塞返回,提示连接成功
连接成功:
此时,accept()函数会返回一个新的socket描述符,服务端可以用socket描述符与客户端进行通信,之前的那个server_socket继续用来监听接口
四次挥手
这个过程是由一方或者双方调用close()触发的
第一次挥手:客户端说,我这边已经问完问题了(请求结束),close(client_fd),此时客户端进入半关闭状态(只能发不能收)
第二次挥手:客服说,我知道你说完了(接收到请求结束信息),此时知道对方已经不再发送消息,但是自己可能还有话要说
第三次挥手:客服说,我也说完了,此时服务端执行close(),
第四次挥手;客户最后确认,之后彻底挂断
2.常用API
某些过时的api不再展示,可通过man手册查询
2.1核心套接字函数
函数 | 描述 | 主要用于 |
---|---|---|
socket() | 创建一个套接字,返回一个文件描述符。 | TCP & UDP |
bind() | 将套接字绑定到一个特定的IP地址和端口号。 | TCP & UDP |
listen() | 将 TCP 套接字置于监听状态,等待传入的连接。 | TCP (Server) |
accept() | 接受一个传入的 TCP 连接,返回一个用于通信的新套接字。 | TCP (Server) |
connect() | 客户端向指定的服务器发起 TCP 连接。 | TCP (Client) |
close() | 关闭一个套接字,释放资源。 | TCP & UDP |
socket
//创建套接字设备,返回该设备的文件描述符
int socket(int domain, int type, int protocol);
//参数:
//domain:指定网络通讯的协议家族 IPV4/IPV6
//type:指定通讯的类型 TCP/UDP
//protocol:指定一个特定的用于套接字的协议
//通常只有一个协议支持给特定的协议家族类型,这个时候参数设置为0
//返回值:成功,返回新的用于socket设备的描述符
//失败:-1 errno被设置为相应的错误值
listen
//功能:在指定的套接字上监听连接。
int listen(int socket, int backlog);
//参数:
//socket:指具体的socket设备
//backlog:指定未决连接队列的最大数
//返回值,成功 1 失败-1 errno被设置为相应的错误值
//未决连接:客户端已经发起连接请求(SYN包已到达服务器),但还没有被服务器正式接受(accept())的连接。
bind
//功能:将本地地址和套接字绑定在一起
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
//参数:
//socket:指定具体的socket设备
//struct sockaddr *address:指定本地地址
//address_len:指定本地地址的长度
//返回值:成功返回0,失败返回-1.errno被设置为相应的错误值
//struct sockaddr代表通用地址类型
struct sockaddr {
sa_family_t sa_family; /* Address family */
char sa_data[]; /* Socket address */
};
accept
//功能:在指定套接字上,从套接字的未决连接队列中接收一个连接
int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
//参数:
//socket:指定具体的socket设备.
//address:指定一个具体的地址空间,用于存放客户端的ip地址和端口,具体依赖于地址家族
//addrlen:指定addr的长度,返回实际用到空间的字节数
//返回值:成功 返回连接描述符
//失败 -1 errno被设置为相应的错误值
//struct sockaddr* address
struct sockaddr {
sa_family_t sa_family; /* Address family */
char sa_data[]; /* Socket address */
};
connect
int connect(int socket, const struct sockaddr *address,
socklen_t address_len);
//功能:在指定套接字上,向服务器发起连接
//参数:
//socket:指定具体的socket设备。
//address:指定一个具体的服务器的地址空间。
//addrlen:指定addr的长度。
//返回值:成功 返回连接描述符
//失败 -1 errno被设置为相应的错误值
close
//功能:关闭一个套接字,释放资源
close()
2.2核心数据结构
结构体 | 描述 |
---|---|
struct sockaddr | 通用的套接字地址结构。许多函数接收这个类型的指针作为参数。 |
struct sockaddr_in | IPv4 专用的地址结构。实际使用时需要强制转换为 struct sockaddr*。 |
struct sockaddr_in6 | IPv6 专用的地址结构。 |
struct addrinfo | 由 getaddrinfo() 使用,包含地址、家族、协议等信息。 |
struct sockaddr — 通用套接字地址结构
最根本的结构体,用于强制类型转换,以保证所有地址相关函数的参数类型统一。
#include <sys/socket.h>
struct sockaddr {
sa_family_t sa_family; // 地址族(Address Family),如 AF_INET, AF_INET6
char sa_data[14]; // 地址数据(包含IP地址和端口号)
};
// sa_family: 指定地址族类型。这是一个至关重要的字段,它告诉系统如何解释 sa_data 中的内容。
// AF_INET: IPv4 协议族
// AF_INET6: IPv6 协议族
// AF_UNIX 或 AF_LOCAL: 本地 Unix 域套接字
// sa_data: 一个粗糙的缓冲区,根据不同地址族存放具体的地址信息(IP地址 + 端口号)。
使用原因
像 bind(), connect(), accept(), sendto(), recvfrom() 这些函数被设计成通用函数,它们需要能处理多种协议(IPv4, IPv6, Unix Domain等)的地址。为了避免为每种地址类型都设计一个函数,它们的参数被定义为 struct sockaddr * 类型。这意味着,在传递参数时,你必须将具体的地址结构(如 struct sockaddr_in)的指针强制转换为 struct sockaddr *
几乎不会直接填充这个结构体,而是使用下面更具体的结构体,然后在传参时进行强制类型转换。
struct sockaddr_in — IPv4 套接字地址结构
这是实际编程中用于 IPv4 地址的核心结构体。“in” 代表 “Internet”。
#include <netinet/in.h> // 主要头文件
/**
* @param sin_family: 必须设置为 AF_INET。
* @param sin_port:存储 端口号。关键点:必须使用网络字节序(大端模式)。通常使用 htons() 函数将主机字节序的端口号转换后存入。
* 例如:server_addr.sin_port = htons(8080);
* @param sin_addr:是一个 struct in_addr,其唯一成员 s_addr 存储 32位 IPv4 地址。同样必须使用网络字节序。通常使用 inet_addr() 或 inet_pton() 函数将点分十进制的字符串(如 "192.168.1.1")转换后存入。
* 例如:server_addr.sin_addr.s_addr = inet_addr("192.168.1.1");
* 也可以设置为 INADDR_ANY (htonl(INADDR_ANY)),表示绑定到本机所有可用的IP地址。
* @param sin_zero[8]:没有实际意义它的唯一作用是为了让 struct sockaddr_in 和 struct sockaddr 两个结构体的大小保持一致(都是16字节)。在使用时,我们通常会用 memset() 或 bzero() 将这个字段全部置为0。
*
*/
struct sockaddr_in {
sa_family_t sin_family; // 地址族: 必须为 AF_INET
in_port_t sin_port; // 16位的端口号 (使用网络字节序)
struct in_addr sin_addr; // 32位的IPv4地址
char sin_zero[8]; // 填充字段,通常设置为全0(为了与 struct sockaddr 大小保持一致)
};
// in_addr 结构体的定义
struct in_addr {
in_addr_t s_addr; // 32位的IPv4地址(网络字节序)
};
struct sockaddr_in6 — IPv6 套接字地址结构
用于IPv6地址的结构体
/**
* @param sin6_family: 必须设置为 AF_INET6。
* @param sin6_port: 端口号,网络字节序,同 IPv4。
* @param sin6_flowinfo 和 sin6_scope_id: 用于高级的 IPv6 特性,在一般编程中通常设置为0。
* @param sin6_addr: 存储 128位 IPv6 地址。使用 inet_pton(AF_INET6, "2001:db8::1", &addr.sin6_addr) 之类的函数进行赋值。
*
*/
#include <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族: 必须为 AF_INET6
in_port_t sin6_port; // 16位的端口号 (网络字节序)
uint32_t sin6_flowinfo; // IPv6流信息,通常设为0
struct in6_addr sin6_addr; // 128位的IPv6地址
uint32_t sin6_scope_id; // 范围ID,用于链路本地地址
};
struct in6_addr {
unsigned char s6_addr[16]; // 128位的IPv6地址
};
struct in_addr 和 struct in6_addr
这两个结构体专门用于存储纯IP地址(不包含端口号)。
struct in_addr: 存储一个IPv4地址。
struct in6_addr: 存储一个IPv6地址。
它们常用于诸如 gethostbyname() 等函数返回的信息中。
struct hostent — 主机条目结构体
这是最经典、最传统的主机信息结构体,主要用于 gethostbyname() 和 gethostbyaddr() 函数。但是在现代编程中已经被getaddrinfo() 取代(更健壮、支持 IPv6 和协议无关编程),但理解这个结构体对阅读历史遗留代码和深入理解网络原理仍旧十分重要
缺点:
仅支持 IPv4:gethostbyname() 无法解析 IPv6 地址。
非线程安全:它通常使用静态存储区域,多次调用会覆盖之前的结果。
接口粗糙:需要手动处理二进制地址到字符串的转换。
#include <netdb.h>
/**
* @param h_name:主机名
* @param h_aliases:这是一个指向字符串数组的指针。该数组包含了主机的所有别名(Aliases),最后一个元素是一个 NULL 指针,表示列表结束。例如一个主机可能有一个官方名同时也有别名
* @param h_addrtype:返回的地址类型。对于 gethostbyname(),它通常是你请求的地址族(如 AF_INET)。但要注意,传统的 gethostbyname() 仅支持 IPv4,所以这个值几乎总是 AF_INET。
* @param h_length:每个地址的字节长度。如果 h_addrtype 是 AF_INET,则 h_length 为 4;如果是 AF_INET6,则为 16。
* @param h_addr_list:是最重要的成员,它是一个指向网络字节序 IP 地址列表的指针。列表中的每个元素是一个 char *,但实际上它指向的是一个 in_addr 或 in6_addr 结构(即二进制形式的 IP 地址)。列表以 NULL 指针结束。
* 一个主机名可以对应多个 IP 地址,这个列表就包含了所有地址。
*/
struct hostent {
char *h_name; // 主机的官方规范名称(Official canonical name)
char **h_aliases; // 一个指向别名列表的指针(数组以NULL指针结束)
int h_addrtype; // 地址类型(通常是 AF_INET 或 AF_INET6)
int h_length; // 地址的长度(以字节为单位,IPv4为4,IPv6为16)
char **h_addr_list; // 一个指向主机网络地址列表的指针(数组以NULL指针结束)
};
// 为了兼容性,通常定义了这个别名,指向 h_addr_list[0]
#define h_addr h_addr_list[0]
struct addrinfo — 地址信息结构体
这是一个现代网络编程中极其重要的结构体,主要用于 getaddrinfo() 和 freeaddrinfo() 函数,用于主机名解析和服务名解析,并直接生成可用于 socket() 创建和 bind()/connect() 的地址信息。
优越性:
1.协议无关:通过 ai_family 字段指定 AF_UNSPEC,可以同时获取 IPv4 和 IPv6 地址。
2.线程安全:getaddrinfo() 在堆上分配内存,结果需要手动调用 freeaddrinfo() 释放。
3.功能强大:不仅能解析主机名,还能通过服务名(如 “http”)解析端口号,并直接填充好可用于 socket(), bind(), connect() 的地址结构。
4.返回链表:自然地处理一个主机名对应多个地址的情况。
#include <netdb.h>
/**
* @param ai_family, ai_socktype, ai_protocol: 这三个字段直接对应 socket() 函数的三个参数,用于指定你想要创建的套接字属性。
* @param ai_flags: 提供额外的控制,最常用的是 AI_PASSIVE,表示返回的地址将用于 bind() 服务器套接字(即套接字将监听该地址)。
* @param ai_addr: 一个已经填充好的 sockaddr 结构体(可能是 sockaddr_in 或 sockaddr_in6) 的指针,可以直接用于 bind() 或 connect()。
* @param ai_addrlen: ai_addr 指向的结构体的长度,可直接用于 bind()/connect() 的 addrlen 参数。
* @param ai_next: 这是一个链表指针。因为一个主机名可能对应多个IP地址(IPv4和IPv6),getaddrinfo() 会返回一个 addrinfo 结构链表。
*/
struct addrinfo {
int ai_flags; // 附加选项(如 AI_PASSIVE)
int ai_family; // 地址族(如 AF_INET, AF_INET6, AF_UNSPEC)
int ai_socktype; // 套接字类型(如 SOCK_STREAM, SOCK_DGRAM)
int ai_protocol; // 协议(通常为0,表示自动)
socklen_t ai_addrlen; // ai_addr 指向的地址结构体的长度
struct sockaddr *ai_addr; // 指向 sockaddr 结构体的指针
char *ai_canonname; // 主机的规范名
struct addrinfo *ai_next; // 链表中的下一个结构体
};
2.3 数据传输函数
函数 | 描述 | 主要用于 |
---|---|---|
send() | 通过已连接的 TCP 套接字发送数据 | TCP |
recv() | 从已连接的 TCP 套接字接收数据 | TCP |
sendto() | 发送一个 UDP 数据报。必须指定目标地址 | UDP |
recvfrom() | 接收一个 UDP 数据报。同时获取发送方的地址 | UDP |
write() | 可以向套接字写入数据(等同于 send() 的简单形式) | TCP |
read() | 可以从套接字读取数据(等同于 recv() 的简单形式) | TCP |
2.4 地址转换函数
函数 | 描述 |
---|---|
inet_pton() | 将点分十进制的IP地址字符串(如 “192.168.1.1”)转换为网络字节序的二进制形式(struct in_addr)。【现代首选】 |
inet_ntop() | 将网络字节序的二进制IP地址转换为点分十进制的字符串形式。【现代首选】 |
getaddrinfo() | 【现代首选】 通过主机名或服务名(如 “http”)获取所有地址信息,返回一个 addrinfo 结构体链表,兼容 IPv4 和 IPv6。 |
getnameinfo() | 【现代首选】 getaddrinfo() 的逆操作,通过套接字地址获取对应的主机名和服务名。 |
3.TCP通信模型
TCP 通信需要区分服务器端和客户端
3.1TCP服务器端流程(相当于服务员)
1.创建套接字(通讯端点),返回该端点的文件描述符
2.绑定地址,将该端点和本地地址及端口号绑定
3.监听连接,将该端点设置为被动状态,监听连接到来,有连接的到来,将其放入未决连接队列中
4.接收连接
TCP服务器测试案例
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
// 1.创建套接字
int server_id = socket(AF_INET, SOCK_STREAM, 0);
if (server_id == -1)
{
std::cerr << "socket failed" << strerror(errno) << std::endl;
return -1;
}
// 2.绑定地址和端口
sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
// 表示接收任意主机号
sockaddr.sin_addr.s_addr = INADDR_ANY;
sockaddr.sin_port = htons(8080);
if ((bind(server_id, (struct sockaddr *)&sockaddr, sizeof(sockaddr))) < 0)
{
std::cerr << "bind faild:" << strerror(errno) << std::endl;
close(server_id);
return -1;
}
// 监听连接
if (listen(server_id, 5) < 0)
{
std::cerr << "Listen failed" << std::endl;
close(server_id);
return -1;
}
// 无限循环接收客户端连接
while (true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
// 接收客户端的套接字地址结构
sockaddr_in client_addr;
memset(&sockaddr, 0, sizeof(client_addr));
socklen_t client_addr_len = sizeof(client_addr);
std::cout << "Waiting for client connection..." << std::endl;
// 接受连接
int client_socket = accept(server_id, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket < 0)
{
std::cerr << "failed to accept..." << std::endl;
return -1;
}
// 打印客户端信息
std::cout << "Client connected:" << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
// 处理单个请求
while (true)
{
int bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received <= 0)
{
if (bytes_received == 0)
{
std::cout << "客户端关闭连接" << std::endl;
}
else
{
std::cerr << "Failed to receive" << std::endl;
}
break; // 退出内层循环,等待新连接
}
std::cout << "接收到来自客户端的数据:" << buffer << std::endl;
// 缓冲区清空
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "ok");
if (send(client_socket, buffer, strlen(buffer), 0) < 0)
{
std::cerr << "Filed to send" << std::endl;
break;
}
}
}
}
3.2TCP客户端流程(相当于用户)
1.创建套接字(通讯端点),返回该端点的文件描述符
2.连接服务器,使用返回的文件描述符
3.数据通信
- 向服务器发送请求信息
- 阻塞等待服务器的响应信息
- 处理响应信息
- 关闭通讯端点(文件描述符)结束本次连接
TCP客户端测试案例
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <cstring>
#include <unistd.h>
/**
* 客户端
*/
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Using:./cli 服务器ip 端口号\nExample:./cli 192.168.236.133 6666" << std::endl;
return 1;
}
// 1.创建客户端套接字
/**
* @param AF_INET:协议域,选择ipv4
* @param SOCK_STREAM:套接字类型,可靠,面向连接,基于字节流
* @param 0:协议类型,系统根据指定的"协议域"和"套接字类型"来决定,也可以显式指定
*/
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2.向服务器发起连接
struct sockaddr_in sockaddr;
struct hostent *h;
// 初始化套接字地址结构体
memset(&sockaddr, 0, sizeof(sockaddr));
if ((h = gethostbyname(argv[1])) == 0)
{
std::cout << "gethostbyname failed..." << std::endl;
return -1;
}
// 初始化域,IPv4
sockaddr.sin_family = AF_INET;
// 初始化端口,短整型
sockaddr.sin_port = htons(atoi(argv[2]));
// 将获取到的主机信息赋值给结构体
memcpy(&sockaddr.sin_addr, h->h_addr, h->h_length);
// 请求连接
if (connect(client_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
{
perror("connect");
return -1;
}
// 3.与服务端通信
char buffer[1024];
for (size_t i = 0; i < 3; i++)
{
int inet;
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "这是第%d个人,编号%d。", i + 1, i + 1);
// 向服务端发送请求报文
if ((inet = send(client_fd, buffer, strlen(buffer), 0)) <= 0)
{
perror("error");
}
std::cout << "客户端发送了:" << buffer << std::endl;
// 休眠1m防止发送过快
sleep(1);
// 接收客户端回应的报文
memset(buffer, 0, sizeof(buffer));
if ((inet = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) < 0)
{
std::cerr << "Filed to recv from server" << std::endl;
break;
}
std::cout << "接收到:" << buffer << std::endl;
}
}
4.UDP编程模型
4.1UDP服务端流程
1.创建套接字(通讯端点),返回该端点的文件描述符
2. 绑定地址,将该端点和本地地址及端口号绑定
3. 接收数据,等待客户端发送数据报
4. 处理数据,根据业务逻辑处理接收到的数据
5. 发送响应,向客户端回复数据
6. 关闭套接字,释放资源
代码实例
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Using ./server port\nExample:./server 6666" << std::endl;
}
// 1.创建UDP套接字
int server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd == -1)
{
perror("socket");
return -1;
}
// 2.绑定地址
struct sockaddr_in sockaddr;
memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[1]));
sockaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
{
perror("bind");
close(server_fd);
return -1;
}
std::cout << "UDP Server listening on port " << argv[1] << std::endl;
std::cout << "Press Ctrl+C to stop server" << std::endl;
// 接收和发送数据
// 缓冲区
char buffer[1024];
// 存储接收到的地址
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while (true)
{
memset(buffer, 0, sizeof(buffer));
// 接收数据,会阻塞直到收到数据
int recv_len = recvfrom(server_fd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&client_addr, &client_len);
if (recv_len <= 0)
{
perror("recvfrom");
continue;
}
buffer[recv_len] = '\0'; // 确保字符串正确终止
// 获取客户端信息,指定地址长度
char client_ip[INET_ADDRSTRLEN];
// 二进制转为点分十进制,将机器能明白的转换为人能明白的
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);
std::cout << "Received from " << client_ip << ":" << client_port
<< std::endl;
std::cout << "接受到的信息为:" << buffer << std::endl;
std::string response = "ok";
// 发送回应数据
int send_len = sendto(server_fd, response.c_str(), response.length(), 0,
(struct sockaddr *)&client_addr, sizeof(sockaddr));
if (send_len <= 0)
{
perror("sendto");
}
else
{
std::cout << "Sent response to client" << std::endl;
}
}
}
4.2UDP客户端流程
1.创建套接字(通讯端点),返回该端点的文件描述符
2.设置目标服务器地址
3.发送数据,向服务器发送数据报
4. 接收响应,等待服务器回复
5.关闭套接字,释放资源
代码实例
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <string>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Using:./client 服务器ip 端口号\nExample:./client 127.0.0.1 6666" << std::endl;
return 1;
}
// 1.创建udp套接字
int client_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (client_fd == -1)
{
perror("socket");
return -1;
}
// 2.设置服务器地址
struct sockaddr_in sockaddr;
memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[2]));
// 解析服务器IP地址
if (inet_pton(AF_INET, argv[1], &sockaddr.sin_addr) <= 0)
{
std::cerr << "Invalid address: " << argv[1] << std::endl;
close(client_fd);
return -1;
}
std::cout << "UDP Client ready to send to " << argv[1] << ":" << argv[2] << std::endl;
std::cout << "Type 'quit' to exit" << std::endl;
// 发送和接收数据
char buffer[1024];
int sequence = 1;
while (true)
{
// 准备发送数据
memset(buffer, 0, sizeof(buffer));
std::cout << "Enter message (or 'quit' to exit): ";
std::string input;
std::getline(std::cin, input);
if (input == "quit")
{
break;
}
if (input.empty())
{
continue;
}
// 发送数据到服务器
int send_len = sendto(client_fd, input.c_str(), input.length(),
0, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
if (send_len <= 0)
{
perror("sendto");
continue;
}
std::cout << "Sent: " << input << std::endl;
// 接收服务器响应
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in from_addr;
socklen_t from_addr_len = sizeof(from_addr);
int recv_len = recvfrom(client_fd, buffer, sizeof(buffer) - 1,
0, (struct sockaddr *)&sockaddr, &from_addr_len);
if (recv_len <= 0)
{
if (recv_len == 0)
{
std::cout << "Server closed connection" << std::endl;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
std::cout << "Receive timeout,no response from server" << std::endl;
}
else
{
perror("recvfrom");
}
}
}
else
{
buffer[recv_len] = '\0';
std::cout << "Received from server:" << buffer << std::endl;
}
sequence++;
}
// 关闭套接字
close(client_fd);
std::cout << "Client closed" << std::endl;
return 0;
}
5.TCP编程模型 VS UDP编程模型
特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
连接性 | 面向连接 | 无连接 |
建立过程 | 需要进行三次握手 | 无需连接 |
可靠性 | 高可靠 | 不可靠 |
编程接口 | socket(), bind(), listen(), accept(), connect(), send(), recv() | socket(), bind(), sendto(), recvfrom() |
传输速度 | 慢 | 快 |
传输单位 | 流 | 数据报 |
适用场景 | 对数据准确性要求高,传输大量数据的场景。如:文件传输、邮件、网页浏览 | 对实时性要求高,可容忍部分数据丢失的场景。如:音视频通话、直播、游戏。 |
选择建议:可靠性优先选择TCP,速度优先选择UDP
更多推荐
所有评论(0)