套接字编程入门
什么是 Socket?
套接字 (Socket) 是一种使用标准 UNIX 文件描述符
和其他程序通讯的方式。
如你所知,Unix 中的一切都是文件。所以当你想与另一个程序通讯时,你将通过文件描述符来完成。
套接字描述符
文件描述符是一个整数,它与打开的文件相关联,它可以是任何东西,例如网络连接,管道,终端,或者是磁盘上的真实文件。
因为套接字描述符是文件描述符,所以它可以像文件描述符一样使用。例如,它可以传递给 read
和 write
函数,以便读取和写入数据到套接字。
操作系统使用套接字描述符来跟踪打开的套接字,并识别数据应该发送到哪里或从哪个套接字接收。
socket() 函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// returns: socket descriptor if OK, -1 on error
函数参数
1. 协议族 (Domain)
协议族是一组协议的集合,它们共享相同的套接字地址格式。协议族决定了套接字地址的格式,以及套接字地址中的哪些字段用于指定主机和端口。
常见的协议族有:
AF_INET
IPv4 协议族AF_INET6
IPv6 协议族AF_UNIX
Unix 域协议族AF_UNSPEC
未指定协议族
注意:
AF_
是 Address Family 的缩写。AF_INET
和AF_INET6
域用于 Internet 上的通信。AF_UNIX
和AF_LOCAL
域用于同一台机器上的通信,其中AF_LOCAL
是AF_UNIX
的别名。AF_UNSPEC
域用于指定套接字可以与其他域一起使用。
2. 套接字类型 (Type)
Type 参数决定了套接字的数据传输方式
常见的套接字类型有:
类型 | 描述 |
---|---|
SOCK_STREAM | 提供一个有序、可靠、双向、面向连接的字节流 |
SOCK_SEQPACKET | 提供一个固定长度、有序、可靠、面向连接的消息 |
SOCK_DGRAM | 提供一个固定长度、无连接、不可靠的消息 |
SOCK_RAW | 提供一个 IP 的数据报接口 |
注意:
SOCK_SEQPACKET
类似于SOCK_DGRAM
,但提供了额外的功能,例如错误检查和流控制,以确保数据可靠地按正确的顺序传递。
3. 协议 (Protocol)
Protocol 参数指定了套接字使用的特定协议。它通常是 0,将根据给定的域和套接字类型选择默认协议。
当同一域和套接字类型支持多个协议时,我们可以使用协议参数来选择特定的协议。
注意:
SOCK_STREAM
在AF_INET
域中的默认协议是 TCP (RFC 793)。例如:telnet, ftp, ssh, web browsers 等。SOCK_DGRAM
在AF_INET
域中的默认协议是 UDP (RFC 768)。例如:DNS, DHCP, NTP 等。协议 描述 IPPROTO_IP IPv4 IPPROTO_IPV6 IPv6 IPPROTO_ICMP Internet 控制报文协议 IPPROTO_RAW 原始 IP 数据包 IPPROTO_TCP TCP - 传输控制协议 IPPROTO_UDP UDP - 用户数据报协议
socket 函数如何工作?
调用 socket 函数类似于调用 open 函数。两者都返回一个文件描述符,可以用于后续的 I/O 操作。
当你完成了套接字的使用,可以通过调用 close 函数来关闭套接字,并释放文件描述符以便重用。
尽管套接字实际上是一个文件描述符,但是你不能使用它来调用任何接受文件描述符作为参数的函数。例如:lseek,因为套接字没有文件偏移量。
下面列举了一些常见的文件描述符操作,以及套接字是否支持这些操作。
函数 | 描述 |
---|---|
dup | 复制套接字 |
close | 释放套接字 |
read | 从套接字读取数据,相当于 recv |
write | 向套接字写入数据,相当于 send |
select | 等待套接字准备好进行 I/O 操作 |
mmap | 不支持 |
lseek | 不支持 |
套接字地址
为了确定我们要与之通信的特定进程,我们需要知道进程的地址。
进程地址由 机器的网络地址 和 端口号 构成。
字节序
(图片来源: Hackaday - Don’t Let Endianness Flip You Around)
当我们与同一台机器上的进程通信时,我们不需要担心数据的字节顺序。
但是,当我们与不同机器上的进程通信时,我们需要确保数据的字节顺序在两台机器上是相同的。
大端序和小端序
大端序:数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。
大端序的排列方式与数据用字节表示时的书写顺序一致,符合人类的阅读习惯。
big-endian example:
| n | n + 1 | n + 2 | n +3 |
|-----|-------|-------|------|
| MSB | | | LSB |
小端序: 将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序。
小端序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。
| n + 3 | n + 2 | n + 1 | n |
|-------|-------|-------|-----|
| MSB | | | LSB |
通常,网络协议将指定字节顺序,以便数据可以在不混淆字节顺序的情况下在不同的机器之间交换。
- TCP/IP 协议套件使用大端序。
字节序转换函数
函数 | 描述 |
---|---|
htons | 主机字节序到网络字节序的转换(16位) |
htonl | 主机字节序到网络字节序的转换(32位) |
ntohs | 网络字节序到主机字节序的转换(16位) |
ntohl | 网络字节序到主机字节序的转换(32位) |
h
是 host
的缩写,n
是 network
的缩写。
l
是 long
的缩写,s
是 short
的缩写。
下面是这些函数的原型:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
地址结构
一个地址标识了一个特定协议通信域中的一个套接字端点。
在不同的操作系统可能会有不同的地址结构实现。
例如在 FreeBSD 中,地址结构定义如下:
struct sockaddr {
unsigned char sa_len; /* 总长度 */
sa_family_t sa_family; /* 地址族,如 AF_INET */
char sa_data[14]; /* 固定长度的地址信息 */
};
在 Linux 系统中,地址结构如下:
struct sockaddr {
sa_family_t sa_family; /* 协议族,如 AF_INET */
char sa_data[14]; /* 固定长度的地址信息 */
};
sa_family
通常会是 AF_INET(IPv4)或 AF_INET6(IPv6)。
sa_data
是一个长度为 14 字节的字符数组,用于存放地址信息,例如目标 IP 地址和端口号。
IPv4 地址结构
IPv4 地址结构在头文件netinet/in.h
中定义如下:
// 定义在 <sys/socket.h>
typedef uint16_t sa_family_t; // short, 2 字节
// 定义在 <netinet/in.h>
typedef uint16_t in_port_t; // unsigned short, 2 字节
typedef uint32_t in_addr_t; // unsigned long, 4 字节
struct in_addr {
in_addr_t s_addr; /* 32 位 IPv4 地址, 网络字节序(大端序) */
};
struct sockaddr_in {
sa_family_t sin_family; /* 地址族,如 AF_INET */
in_port_t sin_port; /* 端口号,网络字节序 */
struct in_addr sin_addr; /* IPv4 地址,网络字节序 */
unsigned char sin_zero[8]; /* 用于填充,使其总长度为 16 字节 */
};
IPv6 地址结构
IPv6 地址结构在头文件netinet/in.h
中定义如下:
// 定义在 <sys/socket.h>
typedef uint16_t sa_family_t; // short, 2 字节
// 定义在 <netinet/in.h>
typedef uint16_t in_port_t; // unsigned short, 2 字节
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 地址,16 字节
};
// Defined in <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; /* 地址族,如 AF_INET6, 2 字节 */
in_port_t sin6_port; /* 端口号,网络字节序,2 字节 */
uint32_t sin6_flowinfo; /* IPv6 流信息,4 字节 */
struct in6_addr sin6_addr; /* IPv6 地址,16 字节 */
uint32_t sin6_scope_id; /* IPv6 作用域 ID,4 字节 */
};
// 共计 28 字节
尽管 sockaddr_in
和 sockaddr_in6
结构体不同,但是它们都会被转换为 sockaddr
结构体传递给 socket 系统调用。
sockaddr_storage 结构
sockaddr_storage
结构体被设计为足够大,可以容纳任何地址结构。因为你不知道你将要处理的地址结构是什么,所以你可以使用这个结构体来容纳任何地址结构,然后在需要使用它时将其转换为适当的类型。
sockaddr_storage
结构体在头文件sys/socket.h
中定义如下:
struct sockaddr_storage {
sa_family_t ss_family; /* 2 字节, 地址族,如 AF_INET, AF_INET6 */
// 以下字段是实现定义的
char __ss_pad1[_SS_PAD1SIZE]; /* 6 字节 (size_t)_SS_PAD1SIZE = 6 */
int64_t __ss_align; /* 8 字节 */
char __ss_pad2[_SS_PAD2SIZE]; /* 112 字节 (size_t)_SS_PAD2SIZE = 112 */
};
// 共计 128 字节
IP 地址格式转换
常见的 IP 地址格式如 192.168.0.1
是一种数字和点的格式,但是在计算机中存储的是二进制格式。
为了打印出可读的地址格式,我们使用 inet_ntop
函数将电脑中二进制的地址转换为数字和点的格式,其中 ntop
代表 “network to presentation”。
为了将可读的地址格式转换为二进制格式,我们使用 inet_pton
函数,其中 pton
代表 “presentation to network”。
这两个函数都是线程安全的。
函数原型
inet_ntop
和 inet_pton
函数在头文件 arpa/inet.h
中定义如下:
#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
// 返回值:若成功则返回指向 str 的指针,若出错则返回 NULL
int inet_pton(int domain, const char *restrict str, void *restrict addr);
// 返回值:若成功则返回 1,若输入不是有效的地址字符串则返回 0,若出错则返回 -1
返回值
- inet_ntop:若成功则返回指向 str 的指针,若出错则返回 NULL
- inet_pton:若成功则返回 1,若输入不是有效的地址字符串则返回 0,若出错则返回 -1
参数说明
- domain: 地址族,如 AF_INET, AF_INET6
- addr: 指向二进制地址的指针
- str: 指向存储可读地址的缓冲区的指针
- size: str 缓冲区的大小
通常,我们会使用
INET_ADDRSTRLEN
或INET6_ADDRSTRLEN
宏来指定缓冲区的大小,它们的值如下:- INET_ADDRSTRLEN :用于 IPv4 地址,它的值为 16 字节
- INET6_ADDRSTRLEN :用于 IPv6 地址,它的值为 46 字节
示例代码
struct sockaddr_in sa;
struct sockaddr_in6 sa6;
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr));
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
// IPv4:
char ip4[INET_ADDRSTRLEN]; // 用于存储 IPv4 地址的字符串
struct sockaddr_in sa; // 假设这个结构体已经被加载了一些东西
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // 用于存储 IPv6 地址的字符串
struct sockaddr_in6 sa6; // 假设这个结构体已经被加载了一些东西
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
注意:
上面的代码不够健壮,因为没有检查 IP 地址是否有效。
inet_pton
函数在出错时返回 -1,如果输入不是指定地址族中有效的地址,则返回 0,成功时返回 1。
因此,检查返回值是否大于 0,以确保转换成功。
获取主机名和 IP 地址
getaddrinfo()
在旧的 Unix 系统中,有两个函数可以将主机名转换为 IP 地址,或将 IP 地址转换为主机名。它们是 gethostbyname
和 gethostbyaddr
。
但是,现在使用 getaddrinfo
和 getnameinfo
函数来代替它们,因为它们更加健壮,更加可移植。
下面是 getaddrinfo
函数的原型:
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
getaddrinfo() 函数有三个输入参数,然后将结果存储在 res 中,它返回一个整数,如果成功则为 0,如果出错则为非 0 值。
第一个参数,node,是主机名或 IP 地址。如果它是 NULL,则 IP 地址设置为 IPv4 的 INADDR_ANY 或 IPv6 的 INADDR6_ANY_INIT。
第二个参数,service,是服务名称或端口号。如果它是 NULL,则端口号设置为 0。它可以是十进制数或服务名称,例如 “http” 或 “ftp”。更多查看:IANA 常见端口号列表
第三个参数,hints,是一个指向 struct addrinfo 的指针。它用于指定我们想要的套接字类型。如果它是 NULL,则默认为 SOCK_STREAM(TCP)和 AF_INET(IPv4)
第四个参数,res,是一个指向 struct addrinfo 的指针的指针。它将指向一个链表,其中包含主机名或 IP 地址的信息。
下面是 getaddrinfo() 的用法示例:
struct addrinfo{
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof(struct addrinfo)); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // Don't care if IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // Fill in my IP for me
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo 现在指向一个结构体 addrinfo 的链表,链表中有 1 个或多个结构体
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // 释放 servinfo 结构体
下面是一个客户端的示例,它想连接到服务器(www.example.com)的 3490 端口。
此时并没有连接到服务器,但它为之后的连接做了准备。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
status = getaddrinfo("www.example.com", "3490", &hints, &servinfo);
getnameinfo()
getnameinfo()
函数是 getaddrinfo()
函数的逆函数:它将套接字地址转换为相应的主机和服务。
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);
函数返回值
如果成功,返回 0,然后 node 和 service 名称将填充为 null-terminated 字符串,可能会截断以适合指定的缓冲区长度。
如果出错,返回一个非零错误代码,缓冲区的内容是未定义的。
示例代码
struct sockaddr_in6 sa; // could be IPv4 if you want
char host[1024];
char service[20];
// pretend sa is full of good information about the host and port...
getnameinfo(&sa, sizeof sa, host, sizeof host, service, sizeof service, 0);
printf("host: %s\n", host); // e.g. "www.example.com"
printf("service: %s\n", service); // e.g. "http"
bind() 函数
函数用途
当一个 socket 被创建后,它必须绑定到一个地址和端口号,以便其他进程可以连接到它。
通常服务器会调用 bind
函数来绑定一个 socket 到一个特定的端口号和 IP 地址。
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
函数参数
sockfd
: 套接字文件描述符my_addr
: 指向 struct sockaddr 的指针,它包含服务器的端口号和 IP 地址。addrlen
: my_addr (struct sockaddr) 的的长度。
函数返回值
如果成功,返回 0,如果出错,返回 -1 并设置 errno。
示例代码 - 手动设置 IP 地址和端口号
// !!! THIS IS THE OLD WAY !!!
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
示例代码 - 使用 getaddrinfo()
struct addrinfo hints, *res;
int sockfd;
// 首先,使用 getaddrinfo() 函数加载地址结构:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me (IP of the host it’s running on)
getaddrinfo(NULL, "3490", &hints, &res);
// 然后,使用 socket() 函数创建一个 socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// 最后,使用 bind() 函数将 socket 绑定到指定的端口号和 IP 地址:
bind(sockfd, res->ai_addr, res->ai_addrlen);
connect() 函数
函数用途
通常客户端会调用 connect
函数来连接到服务器。
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
函数参数
sockfd
: 套接字文件描述符serv_addr
: 指向 struct sockaddr 的指针,它包含服务器的端口号和 IP 地址。addrlen
: serv_addr (struct sockaddr) 的的长度。
函数返回值
如果成功,返回 0,如果出错,返回 -1 并设置 errno。
示例代码
struct addrinfo hints, *res;
int sockfd;
// 首先,使用 getaddrinfo() 函数加载地址结构:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
// 其次,使用 socket() 函数创建一个 socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// 最后,使用 connect() 函数连接到服务器:
connect(sockfd, res->ai_addr, res->ai_addrlen);
注意:在上面的示例代码中,我们没有调用 bind() 函数。这是因为我们不关心本地端口号。我们只关心连接方的端口号。这种情况,操作系统内核会自动为我们分配一个端口号,而远程服务器会自动从我们那里获取该端口号。
listen() 函数
函数用途
服务器调用 listen
函数来监听来自客户端的连接请求。
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数参数
sockfd
: 套接字文件描述符backlog
: 最大的连接请求队列的长度。如果队列满了,客户端会收到一个错误信息,错误码为 ECONNREFUSED。
函数返回值
如果成功,返回 0,如果出错,返回 -1 并设置 errno。
示例代码
getaddrinfo();
socket();
bind();
listen();
/* accept() goes here */
如果要让服务器运行在指定的端口号上,需要在调用 listen
函数之前调用 bind
函数。
accept() 函数
函数用途
通常服务器会调用 accept
函数来接受来自特定端口号的客户端的连接请求。
函数原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数参数
sockfd
: 套接字文件描述符addr
: 指向 struct sockaddr 的指针,它包含客户端的端口号和 IP 地址。addrlen
: addr (struct sockaddr) 的的长度。
函数返回值
如果成功,返回一个新的套接字文件描述符(非负整数),如果出错,返回 -1 并设置 errno。
示例代码
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
int main(void){
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, MYPORT, &hints, &res);
// make a socket, bind it, and listen on it:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// now accept an incoming connection:
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
// ready to communicate on socket descriptor new_fd!
}
注意:
所有的 send() 和 recv() 函数都是在 accept() 返回的新的套接字文件描述符上调用的。
如果只想接受一个连接,可以在调用
accept
函数之后调用close(sockfd)
来关闭监听套接字,以防止其他连接。
send() 和 recv() 函数
函数用途
send
和 recv
函数用于客户端和服务器之间的流套接字或已连接的数据报套接字 (datagram socket) 之间的通信。
对于未连接的数据报套接字,使用 sendto
和 recvfrom
函数。
函数原型 - send()
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
函数参数 - send()
sockfd
: 套接字文件描述符msg
: 指向要发送的消息的缓冲区的指针。len
: 要发送的消息的长度 (以字节为单位)。flags
: 标志符,用于修改 send() 的行为。
函数返回值 - send()
返回实际发送的字节数,其值可能小于 len
参数的值。
如果出错,返回 -1 并设置 errno。
示例代码 - send()
char *msg = "Hello, world!";
int len;
int bytes_sent;
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
函数原型 - recv()
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, size_t len, int flags);
函数参数 - recv()
sockfd
: 套接字文件描述符buf
: 指向接收缓冲区的指针。len
: 缓冲区的最大长度 (以字节为单位)。flags
: 标志符,用于修改 recv() 的行为。
函数返回值 - recv()
返回实际读取到缓冲区的字节数。
如果出错,返回 -1 并设置 errno。
如果连接已被关闭,返回 0。
示例代码 - recv()
char buf[100];
int bytes_received;
bytes_received = recv(sockfd, buf, 100, 0);
sendto() 和 recvfrom() 函数
函数用途
因为数据报套接字没有连接到对方,所以在发送消息之前,我们需要提供目标地址。
sendto
和 recvfrom
函数用于客户端和服务器之间的未连接的数据报套接字之间的通信。
函数原型 - sendto()
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, socklen_t tolen);
函数参数 - sendto()
sockfd
: 套接字文件描述符msg
: 指向要发送的消息的缓冲区的指针。len
: 要发送的消息的长度 (以字节为单位)。flags
: 标志符,用于修改 sendto() 的行为。to
: 指向包含目标地址的 struct sockaddr 的指针。它可以是 struct sockaddr_in 或 struct sockaddr_in6。tolen
: 整数,可以简单设置为 sizeof(struct sockaddr_storage)。
函数返回值 - sendto()
返回实际发送的字节数,其值可能小于 len
参数的值。
如果出错,返回 -1 并设置 errno。
示例代码 - sendto()
char *msg = "Hello, world!";
int len;
int bytes_sent;
struct sockaddr_storage their_addr;
socklen_t addr_len;
len = strlen(msg);
bytes_sent = sendto(sockfd, msg, len, 0, (struct sockaddr *)&their_addr, addr_len);
函数原型 - recvfrom()
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd, void *buf, size_t len, unsigned int flags,
struct sockaddr *from, socklen_t *fromlen);
函数参数 - recvfrom()
sockfd
: 套接字文件描述符buf
: 指向接收缓冲区的指针。len
: 缓冲区的最大长度 (以字节为单位)。flags
: 标志符,用于修改 recvfrom() 的行为。from
: 指向包含源地址的 struct sockaddr 的指针。它可以是 struct sockaddr_in 或 struct sockaddr_in6。fromlen
: 指向一个整数的指针,该整数应该初始化为 sizeof(struct sockaddr_storage)。
函数返回值 - recvfrom()
返回实际读取到缓冲区的字节数。
如果出错,返回 -1 并设置 errno。
示例代码 - recvfrom()
char buf[100];
int bytes_received;
struct sockaddr_storage their_addr;
socklen_t addr_len;
bytes_received = recvfrom(sockfd, buf, 100, 0, (struct sockaddr *)&their_addr, &addr_len);
附加信息
如果你的程序使用 connect() 函数连接了一个数据报套接字,你可以使用 send() 和 recv() 函数来代替 sendto() 和 recvfrom() 函数。
套接字本身仍然是一个数据报套接字,数据包仍然使用 UDP,但是套接字接口会自动为你添加目标和源信息。
close() 函数
函数用途
close()
函数将关闭一个文件描述符,这样它就不再指向任何文件,可以被重用。
这将阻止对套接字的任何进一步读取和写入。任何尝试在远程端读取或写入套接字的人都会收到错误。
函数原型
int close(int sockfd);
函数参数
sockfd
: 套接字文件描述符
shutdown() 函数
函数用途
The shutdown()
function disables further send or receive operations on a socket.
函数原型
#include <sys/socket.h>
int shutdown(int sockfd, int how);
// returns: 0 if OK, -1 on error
函数参数
sockfd
: 套接字文件描述符how
: 指定不再允许的操作类型
值 | 常量 | 描述 |
---|---|---|
0 | SHUT_RD | 禁用进一步的接收操作。 |
1 | SHUT_WR | 禁用进一步的发送操作。 |
2 | SHUT_RDWR | 禁用进一步的发送和接收操作。 |
函数返回值
如果成功,返回 0。如果出错,返回 -1 并设置 errno。
getpeername() 函数
函数用途
getpeername()
函数检索与套接字连接的主机名称。
函数原型
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// returns: 0 if OK, -1 on error
gethostname() 函数
函数用途
gethostname()
会将本机名称写入到 hostname 指向的缓冲区中,该缓冲区的长度为 len 字节。
函数原型
#include <unistd.h>
int gethostname(char **hostname, size_t len);
// returns: 0 if OK, -1 on error
最后修改于 2022-12-30
感谢您的支持 :D