第七章:网络编程

  1. 1. 跨主机的传输要注意的问题
    1. 1.1. 字节序问题
      1. 1.1.1. 字节序转换函数
      2. 1.1.2. 格式转换函数
        1. 1.1.2.1. inet_pton(3)
        2. 1.1.2.2. inet_ntop(3)
    2. 1.2. 对齐
    3. 1.3. 类型长度问题
  2. 2. 套接字
    1. 2.1. socket(2)
    2. 2.2. 报式套接字
      1. 2.2.1. 常用函数
        1. 2.2.1.1. bind(2)
        2. 2.2.1.2. sendto(2)
        3. 2.2.1.3. recvfrom(2)
      2. 2.2.2. 多播
        1. 2.2.2.1. setsockopt()
    3. 2.3. 流式套接字

介绍网络编程中的套接字相关内容:报式套接字、流式套接字(待更新)

注:标题中显示的函数数字表示该函数在man手册中所在章节(第2章的是系统调用函数,第3章的是标准函数)

从系统开发角度来讲,作为程序员的必备素质:精通两门编程语言,精通一门脚本(Python),扎实的网络知识。

socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别在于管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递

套接字通信分两部分:

  • 服务器端:被动接受连接,一般不会主动发起连接;
  • 客户端:主动向服务器发起连接;

跨主机的传输要注意的问题

字节序问题

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

  • 大端字节序(Big-Endian):低地址处放高字节;
  • 小端字节序(Little-Endian):低地址处放低字节;

字节序转换函数

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。(两字节的长度用”s”表示,四字节的长度用”l”表示)

  • h - host 主机,主机字节序;
  • n - network 网络字节序;
  • s - short unsigned short;
  • l - long unsigned int;

网络通信时,需要将主机字节序转换成网络字节序(大端),另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。

1
2
3
4
5
6
7
8
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 -> 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 -> 网络字节序

// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 -> 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 -> 网络字节序

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

int main()
{
// htons 转换端口
unsigned short a = 0x1234;
printf("a = 0x%x\n", a);
unsigned short b = htons(a);
printf("b = 0x%x\n", b);

// htonl 转换IP
unsigned char buf[4] = {192, 168, 34, 129};
uint32_t num = *(uint32_t *)buf;
uint32_t sum = htonl(num);
unsigned char *p = (char *)&sum;
printf("%d.%d.%d.%d\n", *p, *(p + 1), *(p + 2), *(p + 3));

#if 0
// ntohl
unsigned char buf1[4] = {128, 34, 168, 192};
uint32_t num1 = *(uint32_t *)buf1;
uint32_t sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d.%d.%d.%d\n", *p1, *(p1 + 1), *(p1 + 2), *(p1 + 3));
#endif

unsigned char buf2[4] = {128, 34, 168, 192};
uint32_t ip = *(uint32_t *)buf2;
uint32_t network_order = htonl(ip);
// 将网络字节序的IP地址转换为点分十进制格式
struct in_addr addr;
addr.s_addr = network_order;
char *ip_str = inet_ntoa(addr);
printf("IP address: %s\n", ip_str);

return 0;
}

编译运行结果如下:

1
2
3
4
a = 0x1234
b = 0x3412
129.34.168.192
IP address: 192.168.34.128

格式转换函数

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,要把整数表示的 IP 地址转化为可读的字符串。inet_pton(),inet_ntop() 函数可用于 点分十进制字符串 表示的 IPv4 地址和 二进制格式的网络字节序整数 表示的 IPv4 地址之间的转换,并且它们适用 IPv4 地址和 IPv6 地址:

inet_pton(3)

inet_pton() 函数(”presentation to network”)用于将字符串格式的IP地址转换为二进制格式的网络字节序整数。函数原型如下:

1
2
3
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);

参数说明:

  • af:地址族。常见的值有:
    • AF_INET:IPv4协议;
    • AF_INET6:IPv6协议;
  • src:指向以null结尾的字符串格式的源IP地址。
  • dst:指向目标结构的指针,用于存储转换后的二进制格式的IP地址。对于IPv4,它是一个struct in_addr类型的指针;对于IPv6,它是一个struct in6_addr类型的指针。

函数返回值:

  • 成功时,返回 1;
  • 输入不是有效的IP地址格式时,返回 0;
  • 发生错误时,返回 -1,并设置 errno

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

int main() {
const char *ip_str = "192.168.1.1";
struct in_addr ip_addr;

if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) {
perror("inet_pton error");
exit(EXIT_FAILURE);
}

printf("Binary format of IP: %u\n", ntohl(ip_addr.s_addr));
exit(EXIT_SUCCESS);
}

编译运行结果如下:

1
Binary format of IP: 3232235777
inet_ntop(3)

INET_NTOP(3)

NAME

inet_ntop - convert IPv4 and IPv6 addresses from binary to text form

SYNOPSIS

#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

DESCRIPTION

This function converts the network address structure src in the af address family into a character string. The resulting string is copied to the buffer pointed to by dst, which must be a non-null pointer. The caller specifies the number of bytes available in this buffer in the argument size.

inet_ntop() extends the inet_ntoa(3) function to support multiple address families, inet_ntoa(3) is now considered to be deprecated in favor of inet_ntop(). The following address families are currently supported:

AF_INET src points to a struct in_addr (in network byte order) which is converted to an IPv4 network address in the dotted-decimal format, “ddd.ddd.ddd.ddd”. The buffer dst must be at least INET_ADDRSTRLEN bytes long.

AF_INET6 src points to a struct in6_addr (in network byte order) which is converted to a representation of this address in the most appropriate IPv6 network address format for this address. The buffer dst must be at least INET6_ADDRSTRLEN bytes long.

RETURN VALUE

On success, inet_ntop() returns a non-null pointer to dst. NULL is returned if there was an error, with errno set to indicate the error.

inet_ntop() 函数(”network to presentation”)用于将二进制格式的IP地址转换为字符串格式。函数原型如下:

1
2
3
#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数说明:

  • af:地址族。常见的值有:
    • AF_INET:IPv4协议;
    • AF_INET6:IPv6协议;
  • src:指向二进制格式源IP地址的指针。
  • dst:指向存储转换后字符串格式IP地址的缓冲区的指针。
  • sizedst缓冲区的大小。对于IPv4地址,它应至少为INET_ADDRSTRLEN,对于IPv6地址,至少为INET6_ADDRSTRLEN

函数返回值:

  • 成功时,返回一个指向目标缓冲区的指针,该缓冲区包含转换后的字符串;
  • 失败时,返回 NULL 并设置 errno

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

int main() {
struct in_addr ip_addr;
ip_addr.s_addr = htonl(3232235777); // 192.168.1.1 in binary
char str_addr[INET_ADDRSTRLEN];

if (inet_ntop(AF_INET, &ip_addr, str_addr, sizeof(str_addr)) == NULL) {
perror("inet_ntop error");
exit(EXIT_FAILURE);
}

printf("String format of IP: %s\n", str_addr);
exit(EXIT_SUCCESS);
}

编译运行结果如下:

1
String format of IP: 192.168.1.1

对齐

对齐方式不一样的解决方法是不对齐。

类型长度问题

比如int类型占多大,char型有没有符号,标准C中对这些问题都没有严格的指定。

解决方法:比如int32_t(32位有符号整型数),uint32_t(32位无符号整型数),int8_t,uint8_t。

套接字

背景:在计算机网络发展的早期,不同的网络协议和技术都有各自独特的接口和调用方式。例如,某些接口可能只支持 TCP/IP 协议,而另一些接口可能只支持其他类型的网络协议。这使得网络编程变得相当复杂和混乱。

为了解决这个问题,计算机科学家在 UNIX 操作系统中引入了套接字(Socket)的概念。套接字提供了一个统一的网络编程接口,使得程序员可以使用相同的函数和数据结构,不管底层使用的是什么网络协议。socket() 函数就是创建套接字的函数。

底层用哪种协议,上层用哪种实现方式都不好确定,于是抽象出来了socket机制,socket是个中间层,返回一个文件描述符。

socket(2)

SOCKET(2)

NAME

socket - create an endpoint for communication

SYNOPSIS

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

DESCRIPTION

socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in <sys/socket.h>. The formats currently understood by the Linux kernel include:

Name Purpose Man page

AF_UNIX Local communication unix(7)

AF_INET IPv4 Internet protocols ip(7)

AF_INET6 IPv6 Internet protocols ipv6(7)

Further details of the above address families, as well as information on several other address families, can be found in address_families(7).

The socket has the indicated type, which specifies the communication semantics. Currently defined types are:

SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.

SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

The protocol specifies a particular protocol to be used with the socket. Normally only a single protocol exists to support a particular socket type within a given protocol family, in which case protocol can be specified as 0. However, it is possible that many protocols may exist, in which case a particular protocol must be specified in this manner. The protocol number to use is specific to the “communication domain” in which communication is to take place; see protocols(5). See getprotoent(3) on how to map protocol name strings to protocol numbers.

RETURN VALUE

On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.

ERRORS

EACCES Permission to create a socket of the specified type and/or protocol is denied.

EAFNOSUPPORT The implementation does not support the specified address family.

EINVAL Unknown protocol, or protocol family not available.

EINVAL Invalid flags in type.

EMFILE The per-process limit on the number of open file descriptors has been reached.

ENFILE The system-wide limit on the total number of open files has been reached.

ENOBUFS or ENOMEM Insufficient memory is available. The socket cannot be created until sufficient resources are freed.

EPROTONOSUPPORT The protocol type or the specified protocol is not supported within this domain.

Other errors may be generated by the underlying protocol modules.

socket() 函数用于创建一个新的套接字,并返回一个与之相关的文件描述符。它允许应用程序指定所需的协议族、套接字类型和具体协议。

函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明:

  • domain:指定创建的套接字所用的协议族。常见的值有:
    • AF_INET:IPv4协议;
    • AF_INET6:IPv6协议;
    • AF_UNIX:UNIX域协议;
  • type:指定套接字的类型。常见的值有:
    • SOCK_STREAM:字节流套接字,提供面向连接的、可靠的字节流服务。通常用于 TCP。
    • SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的数据报服务。通常用于 UDP。
    • SOCK_RAW:原始套接字,提供访问底层网络协议的能力,常用于实现新的协议或访问 ICMP 。
  • protocol:指定特定的协议。通常设置为 0,以使用该类型的默认协议。例如,对于 SOCK_STREAM,默认的协议是 TCP。

函数返回值:

  • 成功时,返回一个非负整数,这是新创建的套接字的文件描述符;
  • 失败时,返回 -1, 并设置 errno

注意事项:

  1. sequenced,reliable:指的是只要接收方接到数据,就保证当前包中的数据是正确的(顺序和内容都正确),一定会丢包。
  2. connection-based:从程序员的角度来说,不局限于三次握手,一说基于连接的,那就是点对点的,一对一的,每个人专用的。
  3. 套接字,就像文件描述符一样,是有限的系统资源,当不再需要它们时,应确保关闭它们以避免资源泄露。
  4. 错误处理:socket() 调用可能会因多种原因失败,例如:系统资源不足、指定了无效的参数等。因此,对其返回值进行检查并处理任何错误是很重要的。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

int main()
{
int sockfd;

sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
perror("socket()");
exit(EXIT_FAILURE);
}
printf("Socket created with file descriptor: %d\n", sockfd);

close(sockfd);
exit(EXIT_SUCCESS);
}

编译运行结果如下:

1
Socket created with file descriptor: 3

报式套接字

看一个程序员会不会写网络传输的程序,多半看的是能不能完成报式的传输。报式套接字需要程序员指定和封装的内容稍微丰富一些。

被动端(先运行):

  1. 取得SOCKET;
  2. 给SOCKET取得地址;
  3. 收/发信息;
  4. 关闭SOCKET;

主动端:

  1. 取得SOCKET;
  2. 给SOCKET取得地址(可省略);
  3. 发/收信息;
  4. 关闭SOCKET;

常用函数

bind(2)

函数背景:在网络编程中,为了让服务器能够接受来自客户端的连接,服务器必须在某个网络接口和端口上进行监听。bind() 函数允许我们将套接字与特定的网络接口和端口号关联起来,从而准备接受来自该接口和端口的连接。

BIND(2)

NAME

bind - bind a name to a socket

SYNOPSIS

#include <sys/types.h>

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

DESCRIPTION

When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr. Traditionally, this operation is called “assigning a name to a socket“.

It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive connections (see accept(2)).

The rules used in name binding vary between address families. Consult the manual entries in Section 7 for detailed information. For AF_INET, see ip(7); for AF_INET6, see ipv6(7); for AF_UNIX, see unix(7); for AF_APPLETALK, see ddp(7); for AF_PACKET, see packet(7); for AF_X25, see x25(7); and for AF_NETLINK, see netlink(7).

The actual structure passed for the addr argument will depend on the address family. The sockaddr structure is defined as something like:

struct sockaddr {

sa_family_t sa_family;

char sa_data[14];

}

The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings. See EXAMPLE below.

RETURN VALUE

On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

bind() 函数用于将套接字与特定的IP地址和端口号绑定,它允许服务器指定在哪些网络接口和端口上监听客户端的连接。函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:之前通过 socket() 创建的套接字的文件描述符。
  • addr:指向sockaddr结构的指针,该结构包含要绑定到的IP地址和端口号。实际上,通常我们会使用sockaddr_in(对于IPv4)或sockaddr_in6(对于IPv6)结构,但在调用 bind() 时,我们需要将它们强制转换为sockaddr
  • addrlen:地址结构的大小。

函数返回值:

  • 成功时,返回 0;
  • 失败时,返回 -1,并设置 errno

注意事项:

  1. 将套接字与一个地址(IP 地址和端口号)绑定。这对于服务器程序来说是必要的,因为它 需要在特定的地址上监听客户端的连接请求。该函数接受一个套接字描述符、一个指向地址结构的指针(如 sockaddr_in 或 sockaddr_in6)以及地址结构的大小。

  2. sockaddr 结构:

    在网络编程中,地址结构是用于存储网络地址的。网络地址包括IP地址和端口号等信息,因此在C中,我们需要一种数据结构来存储这些信息。但问题在于,不同的协议族(例如IPv4和IPv6)对地址的表示形式不同,需要的存储空间也不同。例如,IPv4地址需要32位,而IPv6地址需要128位。因此,我们需要一种灵活的方式来表示地址。

    sockaddr结构就是这样一种灵活的表示方式。它包含一个sa_family字段,用于表示地址族(例如AF_INET表示IPv4,AF_INET6表示IPv6),以及一个sa_data字段,用于存储实际的地址信息。由于sa_data是一个字符数组,它可以存储任何形式的地址。

    然而,sockaddr结构在实际使用中并不方便。例如,如果我们要设置一个IPv4地址,我们需要手动将IP地址和端口号等信息编码到sa_data字段中,这是非常麻烦的。因此,通常我们会使用更具体的地址结构,例如sockaddr_in结构(用于IPv4)或sockaddr_in6结构(用于IPv6)。这些结构在sockaddr的基础上增加了一些字段,使得设置地址更方便。

    然后我们在需要传递地址结构的函数(例如bind()和accept())中,我们仍然需要使用sockaddr结构。这是因为这些函数需要处理任何形式的地址,它们不能假设地址一定是IPv4或IPv6形式。因此,我们通常会将sockaddr_in或sockaddr_in6结构强制转换为sockaddr结构,然后传递给这些函数。这就是上面段落中所说的 “将addr的结构指针转换为sockaddr结构,以避免编译器警告” 的含义。实际上,sockaddr结构的唯一用途就是作为这种转换的目标

  3. 选择正确的地址和端口:在多网卡的系统中,确保选择正确的网络接口(或使用 INADDR_ANY 绑定到所有接口)。确保选择的端口号不在其他应用程序使用,通常建议使用大于 1024 的端口号,因为较低的端口号通常保留给标准服务。

  4. 地址复用:如果应用程序可能频繁地启动并立即关闭,或者在短时间内重启,考虑使用 setsockopt() 设置 SO_REUSEADDR 选项,这样可以避免 “address already in use” 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 在写数据的时候不好用
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)];
};

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
int sockfd;
struct sockaddr_in addr;
char ip[16];

sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket()");
exit(EXIT_FAILURE);
}

memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
// addr.sin_addr.s_addr = INADDR_ANY; // Bind to any local IP
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
// perror("bind()");
fprintf(stderr, "bind() failded: %s\n", strerror(errno));
close(sockfd);
exit(EXIT_FAILURE);
}

const char *str = inet_ntop(AF_INET, &addr.sin_addr.s_addr, ip, 16);
printf("Server bound to %s:%d\n", str, ntohs(addr.sin_port));
close(sockfd);
exit(EXIT_SUCCESS);
}

编译运行结果如下:

1
Server bound to 127.0.0.1:8080
sendto(2)

函数背景:在使用 UDP 进行网络通信时,数据是通过单独的数据报文发送的,而不是像 TCP 那样的流式传输。sendto() 函数是专门为无连接的数据报套接字(如 UDP)设计的,它允许开发者将数据发送到指定的远程地址和端口。

函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:要发送数据的套接字的文件描述符。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送的数据的字节数。
  • flags:设置发送操作的各种标志(通常为0)。
  • dest_addr:指向sockaddr结构的指针,该结构包含目标地址和端口信息。
  • addrlen:地址结构的大小。

函数返回值:

  • 成功时,返回实际发送的字节数;
  • 失败时,返回 -1,并设置 errno
recvfrom(2)

函数背景:在使用 UDP 进行网络通信时,由于其无连接的性质,需要一种机制来从数据报套接字接收数据,并同时获取发送方的地址信息。这就是 recvfrom() 函数的作用。

函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:要从中接收数据的套接字的文件描述符。
  • buf:指向用于存储接收数据的缓冲区的指针。
  • len:缓冲区的大小,指定了可以接收的最大字节数。
  • flags:设置接收操作的各种标志(通常为0)。
  • src_addr:指向sockaddr结构的指针,该结构在调用返回时将包含发送方的地址和端口信息。
  • addrlen:是一个输入/输出参数。在调用前,它应该包含src_addr所指向的地址结构的大小,调用返回时,它将包含实际填充的地址的大小。

函数返回值:

  • 成功时,返回接收的字节数;
  • 失败时,返回 -1,并设置 errno

注意事项:

  1. 阻塞行为:默认情况下,recvfrom() 是阻塞的,这意味着如果没有数据可读,它会阻塞直到数据到达。可以通过设置套接字为非阻塞或使用 select/poll 机制来改变这种行为。
  2. 地址信息:recvfrom() 除了返回数据之外,还返回发送方的地址信息,这对于 UDP 是非常有用的,因为这样就可以知道是谁发送的数据,并可以向正确的地址回复。

举个栗子:

客户端发送数据给服务器端,服务器端显示数据,并向客户端发送一条信息表示自己已收到客户端发送来的数据,客户端显示该数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// proto.h

#ifndef PROTO_H__
#define PROTO_H__

#include <stdint.h>

#define NAMESIZE 11
#define BUFSIZE 1024
#define IPSTRSIZE 40

struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// rcver.h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "proto.h"

int main()
{
int sd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len;
struct msg_st msg;
char buf[BUFSIZE] = "Client data sends to Server successfully!";
char ipstr[IPSTRSIZE];

sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0)
{
perror("socket()");
exit(EXIT_FAILURE);
}

memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(1989);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

if (bind(sd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("bind()");
exit(EXIT_FAILURE);
}

addr_len = sizeof(client_addr);
while (1)
{
if (recvfrom(sd, &msg, sizeof(msg), 0, (struct sockaddr *)&client_addr, &addr_len) < 0)
{
perror("recvfrom()");
exit(EXIT_FAILURE);
}
inet_ntop(AF_INET, &client_addr.sin_addr, ipstr, IPSTRSIZE);
printf("---[Server] MESSAGE FROM %s:%d---\n", ipstr, ntohs(client_addr.sin_port));
printf("NAME = %s\n", msg.name); // 单字节数据传输不涉及大端/小端的转换
printf("MATH = %d\n", msg.math);
printf("CHINESE = %d\n", msg.chinese);

if (sendto(sd, buf, strlen(buf), 0, (struct sockaddr *)&client_addr, addr_len) < 0)
{
perror("sendto()");
exit(EXIT_FAILURE);
}
}
close(sd);
exit(EXIT_SUCCESS);
}

编译运行rcver.c程序,然后在另一个终端中执行 netstat -anu 可发现:

1
2
3
4
5
6
7
8
9
10
11
$ netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
udp 0 0 127.0.0.53:53 0.0.0.0:*
udp 0 0 192.168.34.129:68 192.168.34.254:67 ESTABLISHED
udp 0 0 0.0.0.0:631 0.0.0.0:*
udp 0 0 0.0.0.0:54278 0.0.0.0:*
udp 0 0 0.0.0.0:5353 0.0.0.0:*
udp 0 0 0.0.0.0:1989 0.0.0.0:*
udp6 0 0 :::48223 :::*
udp6 0 0 :::5353 :::*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// snder.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "proto.h"

int main(int argc, char **argv)
{
int sd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len;
struct msg_st msg = {"Alan", 90, 95};
char buf[BUFSIZE];
char ipstr[IPSTRSIZE];

if (argc < 2)
{
fprintf(stderr, "Usage: %s <IP>\n", argv[0]);
exit(1);
}

sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0)
{
perror("socket()");
exit(EXIT_FAILURE);
}

memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(1989);
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0)
{
perror("inet_pton()");
exit(EXIT_FAILURE);
}

if (sendto(sd, &msg, sizeof(msg), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("sendto()");
exit(EXIT_FAILURE);
}

addr_len = sizeof(server_addr);
if (recvfrom(sd, buf, BUFSIZE-1, 0, (struct sockaddr *)&client_addr, &addr_len) < 0)
{
perror("recvfrom()");
exit(EXIT_FAILURE);
}
inet_ntop(AF_INET, &client_addr.sin_addr, ipstr, IPSTRSIZE);
printf("---[Client] MESSAGE FROM %s:%d---\n", ipstr, ntohs(client_addr.sin_port));
printf("%s\n", buf);

close(sd);
exit(EXIT_SUCCESS);
}

以上程序可以在同一台主机上运行,也可以在不同主机上运行,先运行rcver程序,然后再另一个终端中执行snder程序。

多播

使用同一个 IP 多播地址接收多播数据包的所有主机构成了一个主机组,也称为多播组。一个多播组的成员是随时变动的,一台主机可以随时加入或离开多播组,多播组成员的数目和所在的地理位置也不受限制,一台主机也可以属于几个多播组。这个我们可以这样理解,多播地址就类似于 QQ 群号,多播组相当于 QQ 群,一个个的主机就相当于群里面的成员。

多播应用:

  • 单点对多点应用:点对多点应用是指一个发送者,多个接收者的应用形式,这是最常见的多播应用形式。典型的应用包括:媒体广播、媒体推送、信息缓存、事件通知和状态监视等。
  • 多点对单点应用:多点对点应用是指多个发送者,一个接收者的应用形式。通常是双向请求响应应用,任何一端(多点或点)都有可能发起请求。典型应用包括:资源查找、数据收集、网络竞拍、信息询问等。
  • 多点对多点应用:多点对多点应用是指多个发送者和多个接收者的应用形式。通常,每个接收者可以接收多个发送者发送的数据,同时,每个发送者可以把数据发送给多个接收者。典型应用包括:多点会议、资源同步、并行处理、协同处理、远程学习、讨论组、分布式交互模拟(DIS)、多人游戏等。

多播编程:

多播程序框架主要包含套接字初始化、设置多播超时时间、加入多播组、发送数据、接收数据以及从多播组中离开几个方面。其步骤如下:

1)建立一个socket。

2)然后设置接收方多播的参数,例如超时时间TTL、本地回环许可LOOP等。

3)设置接收方加入多播组。

4)发送和接收数据。

5)从多播组离开。

1
2
3
4
5
6
7
8
9
10
struct ip_mreq          
{
struct in_addr imn_multiaddr; // 多播组 IP,类似于 群号
struct in_addr imr_interface; // 将要添加到多播组的 IP,类似于 成员号
};
// 当imr_interface 为 INADDR_ANY 时,选择的是默认组播接口。
struct in_addr
{
in_addr_t s_addr;
}

NAME

getsockopt, setsockopt - get and set options on sockets

SYNOPSIS

#include <sys/types.h>

#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

DESCRIPTION

getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd. Options may exist at multiple protocol levels; they are always present at the uppermost socket level.

When manipulating socket options, the level at which the option resides and the name of the option must be specified. To manipulate options at the sockets API level, level is specified as SOL_SOCKET. To manipulate options at any other level the protocol number of the appropriate protocol controlling the option is supplied. For example, to indicate that an option is to be interpreted by the TCP protocol, level should be set to the protocol number of TCP; see getprotoent(3).

The arguments optval and optlen are used to access option values for setsockopt(). For getsockopt() they identify a buffer in which the value for the requested option(s) are to be returned. For getsockopt(), optlen is a value-result argument, initially containing the size of the buffer pointed to by optval, and modified on return to indicate the actual size of the value returned. If no option value is to be supplied or returned, optval may be NULL.

Optname and any specified options are passed uninterpreted to the appropriate protocol module for interpretation. The include file <sys/socket.h> contains definitions for socket level options, described below. Options at other protocol levels vary in format and name; consult the appropriate entries in section 4 of the manual.

Most socket-level options utilize an int argument for optval. For setsockopt(), the argument should be nonzero to enable a boolean option, or zero if the option is to be disabled.

For a description of the available socket options see socket(7) and the appropriate protocol man pages.

RETURN VALUE

On success, zero is returned for the standard options. On error, -1 is returned, and errno is set appropriately.

Netfilter allows the programmer to define custom socket options with associated handlers; for such options, the return value on success is the value returned by the handler.

ERRORS

EBADF The argument sockfd is not a valid file descriptor.

EFAULT The address pointed to by optval is not in a valid part of the process address space. For getsockopt(), this error may also be returned if optlen is not in a valid part of the process address space.

EINVAL optlen invalid in setsockopt(). In some cases this error can also occur for an invalid value in optval (e.g., for the IP_ADD_MEMBERSHIP option described in ip(7)).

ENOPROTOOPT The option is unknown at the level indicated.

ENOTSOCK *The file descriptor sockfd does not refer to a socket.*setsockopt()

setsockopt()

setsockopt() 函数可用于设置或更改套接字的选项。函数原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数说明:

  • sockfd:要设置选项的套接字的文件描述符。
  • level:定义了哪一级别的选项应该被修改。常见的值为 SOL_SOCKET(常规套接字选项), IPPROTO_IP (IP选项), IPPROTO_TCP (TCP选项) 等。
  • optname:指定要设置的选项。例如,SO_REUSEADDR, SO_RCVBUF, SO_LINGER 等。
  • optval:指向包含新选项值的缓冲区的指针。
  • optlenoptval 缓冲区的大小。

函数返回值:

  • 成功时,返回 0;
  • 失败时,返回 -1,并设置 errno

level :

  • IPPROTO_IP

optname:

  • IP_MULTICAST_LOOP 设置或禁止多播数据回送,即多播的数据是否回送到本地回环接口
  • IP_ADD_MEMBERSHIP 加入多播组
  • IP_DROP_MEMBERSHIP 离开多播组

默认情况下,当本机发送组播数据到某个网络接口时,在IP层,数据会回送到本地的回环接口,选项IP_MULTICAST_LOOP 用于控制数据是否回送到本地的回环接口。

使用 IP_ADD_MEMBERSHIP 选项每次只能加入一个网络接口的IP地址到多播组,但并不是一个多播组仅允许一个主机IP地址加入,可以多次调用 IP_ADD_MEMBERSHIP 选项来实现多个IP地址加入同一个广播组,或者同一个IP地址加入多个广播组。

optval:

  • IP_MULTICAST_LOOP 选项对应传入 unsigned int 来确认是否支持多播数据回送
  • IP_ADD_MEMBERSHIP 传入 ip_mreq
  • IP_DROP_MEMBERSHIP 传入 ip_mreq
1
2
3
4
5
6
int loop = 0; // 0 means disable, 1 means enable
if (setsockopt(socket, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0)
{
perror("setsockopt");
exit(1);
}

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// mgroup_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define IP_FOUND "IP_FOUND"
#define IP_FOUND_ACK "IP_FOUND_ACK"
#define MCAST "224.0.0.88"

// 说明:设置主机的TTL值,是否允许本地回环,加入多播组,然后服务器向加入多播组的主机发送数据,主机接收数据,并响应服务器。

int main(int argc, char **argv)
{
int sockfd, client_fd;
struct sockaddr_in localaddr, recvaddr;
socklen_t socklen;
struct ip_mreq mreq;
char recv_buf[20], send_buf[20];
int ttl = 10; // 如果转发的次数等于10,则不再转发
int loop = 0;

sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror(" socket()");
exit(1);
}

memset(&localaddr, 0, sizeof(localaddr));
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(6666);
localaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr *)&localaddr, sizeof(localaddr)) < 0)
{
perror("bind()");
exit(1);
}

// 设置多播的TTL值
if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0)
{
perror("IP_MULTICAST_TTL");
exit(1);
}
// 设置数据是否发送到本地回环接口
if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0)
{
perror("IP_MULTICAST_LOOP");
exit(1);
}
// 加入多播组
mreq.imr_multiaddr.s_addr = inet_addr(MCAST); // 多播组的IP
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本机的默认接口IP,本机的随机IP
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("IP_ADD_MEMBERSHIP");
exit(1);
}

socklen = sizeof(struct sockaddr);
while (1)
{
if (recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&recvaddr, &socklen) < 0)
{
perror("recvfrom()");
exit(1);
}

printf(" recv client addr : %s \n", (char *)inet_ntoa(recvaddr.sin_addr));
printf(" recv client port : %d \n", ntohs(recvaddr.sin_port));
printf(" recv msg : %s \n", recv_buf);

if (strstr(recv_buf, IP_FOUND))
{
// 响应客户端请求,将数据发送给客户端
strncpy(send_buf, IP_FOUND_ACK, strlen(IP_FOUND_ACK) + 1);
if (sendto(sockfd, send_buf, strlen(IP_FOUND_ACK) + 1, 0, (struct sockaddr *)&recvaddr, socklen) < 0)
{
perror("sendto()");
exit(1);
}
printf(" send ack msg to client !\n");
}
}

// 离开多播组
if (setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("IP_DROP_MEMBERSHIP");
exit(1);
}

close(sockfd);
exit(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// mgroup_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define IP_FOUND "IP_FOUND"
#define IP_FOUND_ACK "IP_FOUND_ACK"

/*
广播与多播只支持UDP协议,因为TCP协议是端到端,这与广播与多播的理念相冲突
广播是局域网中一个主机对所有主机的数据通信,而多播是一个主机对一组特定的主机进行通信.多播可以是因特网,而广播只能是局域网。多播常用于视频电话,网上会议等。

setsockopt设置套接字选项可以设置多播的一些相关信息

IP_MULTICAST_TTL //设置多播的跳数值
IP_ADD_MEMBERSHIP //将主机的指定接口加入多播组,以后就从这个指定的接口发送与接收数据
IP_DROP_MEMBERSHIP //主机退出多播组
IP_MULTICAST_IF //获取默认的接口或设置多播接口
IP_MULTICAST_LOOP //设置或禁止多播数据回送,即多播的数据是否回送到本地回环接口

例子:
int ttl=255;
setsockopt(socket,IPPROTO_IP,IP_MULTICAST_TTL,&ttl,sizeof(ttl)); //设置跳数

socket -套接字描述符
PROTO_IP -选项所在的协议层
IP_MULTICAST_TTL -选项名
&ttl -设置的内存缓冲区
sizeof(ttl) -设置的内存缓冲区长度

struct in_addr in;
setsockopt(socket,IPPROTO_IP,IP_MUTLICAST_IF,&in,sizeof(in)); //设置组播接口

int loop=1;
setsockopt(socket,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop)); //设置数据回送到本地回环接口

struct ip_mreq req;
setsockopt(socket,IPPROTO_IP,IP_ADD_MEMBERSHIP,&req,sizeof(req)); //加入组播组
setsockopt(socket,IPPROTO_IP,IP_DROP_MEMBERSHIP,&req,sizeof(req)); //离开组播组

*/

#define MCAST_ADDR "224.0.0.88"

int main(int argc, char **argv)
{
int sockfd;
char send_buf[20];
char recv_buf[20];

struct sockaddr_in server_addr; // 多播地址
struct sockaddr_in our_addr;
struct sockaddr_in recvaddr;
int so_broadcast = 1;

socklen_t socklen;

sockfd = socket(AF_INET, SOCK_DGRAM, 0);

memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;

// server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_addr.s_addr = inet_addr(MCAST_ADDR); // 多播地址
server_addr.sin_port = htons(6666);

// 客户端绑定通信端口,否则系统自动分配
memset(&our_addr, 0, sizeof(our_addr));
our_addr.sin_family = AF_INET;
our_addr.sin_port = htons(7777);
our_addr.sin_addr.s_addr = htonl(INADDR_ANY); // MCAST_ADDR
// 自定义地址如果为有效地址
// 则协议栈将自定义地址与端口信息发送到接收方
// 否则协议栈将使用默认的回环地址与自动端口
// our_addr.sin_addr.s_addr = inet_addr("127.0.0.10");

if (bind(sockfd, (struct sockaddr *)&our_addr, sizeof(our_addr)) < 0)
{
perror("bind()");
exit(1);
}

socklen = sizeof(struct sockaddr);
strncpy(send_buf, IP_FOUND, strlen(IP_FOUND) + 1);

for (int count = 0; count < 1; count++)
{
if (sendto(sockfd, send_buf, strlen(send_buf) + 1, 0, (struct sockaddr *)&server_addr, socklen) != strlen(send_buf) + 1)
{
perror("sendto()");
exit(1);
}

if (recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&recvaddr, &socklen) < 0)
{
perror("recvfrom()");
exit(1);
}

printf(" recv server addr : %s \n", (char *)inet_ntoa(recvaddr.sin_addr));
printf(" recv server port : %d \n", ntohs(recvaddr.sin_port));
printf(" recv server msg : %s \n", recv_buf);
}

close(sockfd);
return 0;
}

运行结果如下:

流式套接字

待更新。。。