7 套接字
约 4388 字大约 15 分钟
2025-06-22
<arpa/inet.h> 套接字类型:
- 流式套接字 基于TCP协议,面向连接,可靠口
- 数据报套接字 基于UDP协议,无连接,不可靠
- 原始套接字 root权限,对网络下层通信协议进行访问。可以自动组装数据包(伪装本地IP和本地MAC)可以接收本机网卡上所有的数据帧(数据包)
客户机在发送数据的时候需要把数据从小端转为大端发送出去,服务器收到数据后需要把数据从大端转为小端 大端在传输时效率更高
各缩写词的含义
缩写 | 含义 |
---|---|
u | unsigned |
16 | 16位;32:32位 |
h | host,主机字节序 |
n | net,网络字节序 |
s | short |
i | int |
常用结构体
// 在写数据的时候不好用
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
struct in_addr{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in{
sa_family_t sin_family; // 地址族协议: AF_INET
in_port_t sin_port; // 端口, 2字节-> 大端
struct in_addr sin_addr; // IP地址, 4字节 -> 大端
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
//0ip地址,也就是0.0.0.0 当使用0ip地址时,会自动获取绑定为主机网卡的ip地址
INADDR_ANY
由于sockaddr类型不好用,可以使用sockaddr_in初始化后转换成sockaddr类型进行使用,这个强制类型转换时的方法和正常的强制转换方法一样
数据类型转换
字节序转换函数
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);
IP地址转换 虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述 所以在进行网络字节序到主机字节序的转换时通常不使用ntoh,因为那是从整型到整型的转换
- af: 地址族(IP地址的家族包括ipv4和ipv6)协议 AF_INET: ipv4格式的ip地址 AF_INET6: ipv6格式的ip地址
- src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
- dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
- 返回值:成功返回1,失败返回0或者-1
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
- af:地址族协议 AF_INET:ipv4格式的ip地址 AF_INET6:ipv6格式的ip地址
- src:传入参数,这个指针指向的内存中存储了大端的整形IP地址
- dst:传出参数,存储转换得到的小端的点分十进制的IP地址
- size:修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
- 返回值: 成功:指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串 失败:NULL
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
ip地址和大端整形数的转换
// 点分十进制IP转换为大端整形
in_addr_t inet_addr (const char *cp);
// 大端整形转换为点分十进制IP
char* inet_ntoa(struct in_addr in);
流式套接字
头文件<arpa/inet.h>
,包含了这个头文件<sys/socket.h>
就不用再包含了
函数
创建套接字 创建一个套接字
- domain: 使用的地址族协议 AF_INET: 使用IPv4格式的ip地址 AF_INET6: 使用IPv4格式的ip地址
- type: SOCK_STREAM: 使用流式的传输协议 SOCK_DGRAM: 使用报式(报文)的传输协议
- protocol: 一般写0即可, 使用默认的协议 SOCK_STREAM: 流式传输默认使用的是tcp SOCK_DGRAM: 报式传输默认使用的udp
- 返回值: 成功返回可用于套接字通信的文件描述符,失败 -1
int socket(int domain, int type, int protocol);
在套接字创建后修改其行为
sockfd
:要设置选项的套接字描述符。这是之前通过socket()
函数创建的套接字。level
:指定选项定义的层次。常见的层次有SOL_SOCKET
(适用于所有类型的套接字的通用选项)、IPPROTO_TCP
(针对TCP协议的选项)、IPPROTO_IP
(针对IP层的选项)等。optname
:要设置的特定选项。例如,SO_REUSEADDR
允许地址重用,TCP_NODELAY
禁用 Nagle 算法等。optval
:指向一个缓冲区,包含选项的具体值。缓冲区的内容和格式取决于所选的optname
。optlen
:optval
缓冲区的大小 成功时返回 0,失败时返回 -1,并设置errno
来指示错误。
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
绑定ip和端口 将文件描述符和本地的IP与端口进行绑定
- sockfd: 监听的文件描述符, 通过socket()调用得到的返回值
- addr: 传入参数, 要绑定的IP和端口信息需要初始化到这个结构体中,
IP和端口要转换为网络字节序
- addrlen: 参数addr指向的内存大小, sizeof(struct sockaddr)
- 返回值:成功返回0,失败返回-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
设置监听(服务端) 给要监听的套接字设置监听
- sockfd: 文件描述符, 可以通过调用socket()得到,在监听之前必须要绑定 bind()
- backlog: 设置同时能处理的最大连接要求,最大值为128(同时处理的最大连接要求,实际上能处理的连接请求远大于128)
- 返回值:函数调用成功返回0,调用失败返回 -1
int listen(int sockfd, int backlog);
等待连接请求(服务端) 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
- sockfd: 监听的文件描述符
- addr: 传出参数, 里边存储了建立连接的客户端的地址信息
- addrlen: 传出参数,用于存储addr指向的内存大小
- 返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
这是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
请求连接(客户端) 对于tcp协议的三次握手只需要在客户端调用connect()函数,三次握手就自动进行了。
对于客户端发送和接收数据也需要绑定ip和端口,但客户端并没有使用bind函数,使用connect函数会默认绑定本机ip地址和一个随机的没有被占用的端口
- sockfd: 通信的文件描述符, 通过调用socket()函数就得到了
- addr: 存储了要连接的服务器端的地址信息: iP 和端口,这个IP和端口也需要转换为大端然后再赋值
- addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)
- 返回值:连接成功返回0,连接失败返回-1 成功连接服务器之后, 客户端会自动随机绑定一个端口
服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
发送数据
- fd: 通信的文件描述符, accept() 函数的返回值
- buf: 传入参数, 要发送的字符串
- len: 要发送的字符串的长度
- flags: 特殊的属性, 一般不使用, 指定为 0
- 返回值: 大于0:实际发送的字节数,和参数len是相等的 -1:发送数据失败
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
接收数据
- sockfd: 用于通信的文件描述符, accept() 函数的返回值
- buf: 指向一块有效内存, 用于存储接收是数据
- size: 参数buf指向的内存的容量
- flags: 特殊的属性, 一般不使用, 指定为 0
- 返回值: 大于0:实际接收的字节数 0:对方断开了连接 -1:接收数据失败了 如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
断开连接 四次挥手是断开连接的过程,需要双向断开,关于由哪一端先断开连接是没有要求的。通信的两端如果想要断开连接就需要调用close()函数,当两端都调用了该函数,四次挥手也就完成了。 客户端和服务器断开连接 -> 单向断开 服务器和客户端断开连接 -> 单向断开 进行了两次单向断开,双向断开就完成了,每进行一次单向断开,就会完成两次挥手的动作。
如果关闭成功了返回0,关闭失败返回-1
int close(int fd);
服务器端通信流程
[!NOTE] 在tcp的服务器端, 有两类文件描述符
- 监听的文件描述符
只需要有一个,不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
- 通信的文件描述符
负责和建立连接的客户端通信,如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符
一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
- 监听的文件描述符: 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中 读缓冲区中有数据, 说明有新的客户端连接,调用accept()函数, 这个函数会检测监听文件描述符的读缓冲区 检测不到数据, 该函数阻塞,如果检测到数据, 解除阻塞, 新的连接建立
- 通信的文件描述符: 客户端和服务器端都有通信的文件描述符 发送数据:调用函数 write() / send(),数据进入到内核中。数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中。内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中 接收数据: 调用的函数 read() / recv(), 从内核读数据。数据如何进入到内核程序猿不需要处理, 数据进入到通信的文件描述符的读缓冲区中。数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可
- 创建用于监听的套接字, 这个套接字是一个文件描述符
int lfd = socket();
- 将得到的监听的文件描述符和本地的IP 端口进行绑定
bind();
- 设置监听(成功之后开始监听, 监听的是客户端的连接)
listen();
- 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的),没有新连接请求就阻塞
int cfd = accept();
- 通信,读写操作默认都是阻塞的
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
- 断开连接, 关闭套接字
close();
客户端的通信流程
- 创建一个通信的套接字
int cfd = socket();
- 连接服务器, 需要知道服务器绑定的IP和端口
connect();
- 通信
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
- 断开连接, 关闭文件描述符(套接字)
close();
Windows套接字通讯
在Window中也提供了套接字通信的API,这些API函数与Linux平台的API函数几乎相同 头文件<winsock2.h> 动态库ws2_32.dll
基于Linux的套接字通信流程是最全面的一套通信流程,其他框架中套接字通信的流程只会更简单,windows套接字流程和linux平台完全相同
函数
定义的结构体类型 windows
typedef struct in_addr {
union {
struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{ unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr; // 存储IP地址
} S_un;
}IN_ADDR;
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
linux
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
加载套接字库 在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源。
初始化库版本号
WORD MAKEWORD(大版本,小版本);
//如2.2版本
MAKEWORD(2, 2);
- wVersionRequested: 使用的Windows Socket的版本, 一般使用的版本是 2.2
- lpWSAData:一个WSADATA结构指针, 这是一个传入参数 创建一个 WSADATA 类型的变量, 将地址传递给该函数的第二个参数
// 初始化Winsock库
// 返回值: 成功返回0,失败返回SOCKET_ERROR。
WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
释放套接字资源 注销Winsock相关库 函数调用成功返回0,失败返回 SOCKET_ERROR。
int WSACleanup ();
创建套接字
返回值: 成功返回套接字, 失败返回INVALID_SOCKET
- af: 地址族协议
ipv4: AF_INET (windows/linux)
PF_INET (windows)
AF_INET == PF_INET - type: 和linux一样
SOCK_STREAM
SOCK_DGRAM - protocal: 一般写0 即可
在windows上的另一种写法
IPPROTO_TCP, 使用指定的流式协议中的tcp协议
IPPROTO_UDP, 使用指定的报式协议中的udp协议
SOCKET socket(int af,int type,int protocal);
关键字: FAR NEAR, 这两个关键字在32/64位机上是没有意义的(在16位机上的寻址方式和32/64位机上是不一样的), 指定的内存的寻址方式
绑定端口 套接字绑定本地IP和端口
返回值: 成功返回0,失败返回SOCKET_ERROR
int bind(SOCKET s,const struct sockaddr FAR* name, int namelen);
设置监听
返回值: 成功返回0,失败返回SOCKET_ERROR
int listen(SOCKET s,int backlog);
等待并接受客户端连接
返回值: 成功返回用于的套接字,失败返回INVALID_SOCKET。
SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
连接服务器
返回值: 成功返回0,失败返回SOCKET_ERROR
int connect (SOCKET s,const struct sockaddr FAR* name,int namelen);
在Qt中connect用户信号槽的连接, 如果要使用windows api 中的 connect 需要在函数名前加::
::connect(sock, (struct sockaddr*)&addr, sizeof(addr));
接收数据 返回值: 成功时返回接收的字节数,收到EOF时为0,失败时返回SOCKET_ERROR。
0 代表对方已经断开了连接
int recv (SOCKET s,char FAR* buf,int len,int flags);
发送数据 返回值: 成功返回传输字节数,失败返回SOCKET_ERROR。
int send (SOCKET s,const char FAR * buf, int len,int flags);
关闭套接字 返回值: 成功返回0,失败返回SOCKET_ERROR
int closesocket (SOCKET s);
udp通信函数
// 接收数据
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR* from,int FAR* fromlen);
// 发送数据
int sendto(SOCKET s,const char FAR* buf,int len,int flags,const struct sockaddr FAR* to,int tolen);
数据类型转换
[!tip] Title window的api中套接字对应的类型是 SOCKET 类型, linux中是 int 类型, 本质是一样的
字节序转换 主机字节序->网络字节序
u_short htons (u_short hostshort );
u_long htonl ( u_long hostlong);
网络字节序 -> 主机字节序
u_short ntohs (u_short netshort );
u_long ntohl ( u_long netlong);
点分十进制IP->大端整形
unsigned long inet_addr (const char FAR * cp);
大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);
贡献者
版权所有
版权归属:PinkDopeyBug