C 语言套接字编程入门

套接字编程入门

什么是 Socket?

套接字 (Socket) 是一种使用标准 UNIX 文件描述符和其他程序通讯的方式。

如你所知,Unix 中的一切都是文件。所以当你想与另一个程序通讯时,你将通过文件描述符来完成。

套接字描述符

文件描述符是一个整数,它与打开的文件相关联,它可以是任何东西,例如网络连接,管道,终端,或者是磁盘上的真实文件。

因为套接字描述符是文件描述符,所以它可以像文件描述符一样使用。例如,它可以传递给 readwrite 函数,以便读取和写入数据到套接字。

操作系统使用套接字描述符来跟踪打开的套接字,并识别数据应该发送到哪里或从哪个套接字接收。

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_INETAF_INET6 域用于 Internet 上的通信。

  • AF_UNIXAF_LOCAL 域用于同一台机器上的通信,其中 AF_LOCALAF_UNIX 的别名。

  • AF_UNSPEC 域用于指定套接字可以与其他域一起使用。

2. 套接字类型 (Type)

Type 参数决定了套接字的数据传输方式

常见的套接字类型有:

类型描述
SOCK_STREAM提供一个有序、可靠、双向、面向连接的字节流
SOCK_SEQPACKET提供一个固定长度、有序、可靠、面向连接的消息
SOCK_DGRAM提供一个固定长度、无连接、不可靠的消息
SOCK_RAW提供一个 IP 的数据报接口

注意:

  • SOCK_SEQPACKET 类似于 SOCK_DGRAM,但提供了额外的功能,例如错误检查和流控制,以确保数据可靠地按正确的顺序传递。

3. 协议 (Protocol)

Protocol 参数指定了套接字使用的特定协议。它通常是 0,将根据给定的域和套接字类型选择默认协议。

当同一域和套接字类型支持多个协议时,我们可以使用协议参数来选择特定的协议。

注意:

  • SOCK_STREAMAF_INET 域中的默认协议是 TCP (RFC 793)。例如:telnet, ftp, ssh, web browsers 等。

  • SOCK_DGRAMAF_INET 域中的默认协议是 UDP (RFC 768)。例如:DNS, DHCP, NTP 等。

    协议描述
    IPPROTO_IPIPv4
    IPPROTO_IPV6IPv6
    IPPROTO_ICMPInternet 控制报文协议
    IPPROTO_RAW原始 IP 数据包
    IPPROTO_TCPTCP - 传输控制协议
    IPPROTO_UDPUDP - 用户数据报协议

socket 函数如何工作?

调用 socket 函数类似于调用 open 函数。两者都返回一个文件描述符,可以用于后续的 I/O 操作。

当你完成了套接字的使用,可以通过调用 close 函数来关闭套接字,并释放文件描述符以便重用。

尽管套接字实际上是一个文件描述符,但是你不能使用它来调用任何接受文件描述符作为参数的函数。例如:lseek,因为套接字没有文件偏移量。

下面列举了一些常见的文件描述符操作,以及套接字是否支持这些操作。

函数描述
dup复制套接字
close释放套接字
read从套接字读取数据,相当于 recv
write向套接字写入数据,相当于 send
select等待套接字准备好进行 I/O 操作
mmap不支持
lseek不支持

套接字地址

为了确定我们要与之通信的特定进程,我们需要知道进程的地址。

进程地址由 机器的网络地址端口号 构成。

字节序

image (图片来源: 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位)

hhost 的缩写,nnetwork 的缩写。

llong 的缩写,sshort 的缩写。

下面是这些函数的原型:

#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_insockaddr_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_ntopinet_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_ADDRSTRLENINET6_ADDRSTRLEN 宏来指定缓冲区的大小,它们的值如下:
    1. INET_ADDRSTRLEN :用于 IPv4 地址,它的值为 16 字节
    2. 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 地址转换为主机名。它们是 gethostbynamegethostbyaddr

但是,现在使用 getaddrinfogetnameinfo 函数来代替它们,因为它们更加健壮,更加可移植。

下面是 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() 函数

函数用途

sendrecv 函数用于客户端和服务器之间的流套接字或已连接的数据报套接字 (datagram socket) 之间的通信。

对于未连接的数据报套接字,使用 sendtorecvfrom 函数。

函数原型 - 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() 函数

函数用途

因为数据报套接字没有连接到对方,所以在发送消息之前,我们需要提供目标地址。

sendtorecvfrom 函数用于客户端和服务器之间的未连接的数据报套接字之间的通信。

函数原型 - 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: 指定不再允许的操作类型
常量描述
0SHUT_RD禁用进一步的接收操作。
1SHUT_WR禁用进一步的发送操作。
2SHUT_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