socket编程
套接字概念
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字通信原理如下图所示:
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
字节序转换
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
网络字节序和主机字节序的转换函数
#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);
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
/* 字节序:字节在内存中存储的顺序。 小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址 大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址 */ // 通过代码检测当前主机的字节序 #include <stdio.h> int main() { union { short value; // 2字节 char bytes[sizeof(short)]; // char[2] } test; test.value = 0x0102; if((test.bytes[0] == 1) && (test.bytes[1] == 2)) { printf("大端字节序\n"); } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) { printf("小端字节序\n"); } else { printf("未知\n"); } return 0; }
/* 网络通信时,需要将主机字节序转换成网络字节序(大端), 另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。 // 转换端口 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); // 主机字节序 - 网络字节序 */ #include <stdio.h> #include <arpa/inet.h> int main() { // htons 转换端口 unsigned short a = 0x0102; printf("a : %x\n", a); unsigned short b = htons(a); printf("b : %x\n", b); printf("=======================\n"); // htonl 转换IP char buf[4] = {192, 168, 1, 100}; int num = *(int *)buf; int sum = htonl(num); unsigned char *p = (char *)∑ printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3)); printf("=======================\n"); // ntohl unsigned char buf1[4] = {1, 1, 168, 192}; int num1 = *(int *)buf1; int sum1 = ntohl(num1); unsigned char *p1 = (unsigned char *)&sum1; printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3)); // ntohs return 0; }
IP地址转换函数
#include <arpa/inet.h> in_addr_t inet_addr(const char *cp); // 把字符串表示的ip转化成网络字节序的整数 int inet_aton(const char *cp, struct in_addr *inp); // 同上,转换结果保存在inp中 char *inet_ntoa(struct in_addr in); // 和1相反
#include <arpa/inet.h> // p:点分十进制的IP字符串,n:表示网络字节序的整数
int inet_pton(int af, const char *src, void *dst); af:地址族: AF_INET AF_INET6 src:需要转换的点分十进制的IP字符串 dst:转换后的结果保存在这个里面 // 将网络字节序的整数,转换成点分十进制的IP地址字符串 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); af:地址族: AF_INET AF_INET6 src: 要转换的ip的整数的地址 dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
/* #include <arpa/inet.h> // p:点分十进制的IP字符串,n:表示network,网络字节序的整数 int inet_pton(int af, const char *src, void *dst); af:地址族: AF_INET AF_INET6 src:需要转换的点分十进制的IP字符串 dst:转换后的结果保存在这个里面 // 将网络字节序的整数,转换成点分十进制的IP地址字符串 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); af:地址族: AF_INET AF_INET6 src: 要转换的ip的整数的地址 dst: 转换成IP地址字符串保存的地方 size:第三个参数的大小(数组的大小) 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的 */ #include <stdio.h> #include <arpa/inet.h> int main() { // 创建一个ip字符串,点分十进制的IP地址字符串 char buf[] = "192.168.1.4"; unsigned int num = 0; // 将点分十进制的IP字符串转换成网络字节序的整数 inet_pton(AF_INET, buf, &num); unsigned char * p = (unsigned char *)# printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3)); // 将网络字节序的IP整数转换成点分十进制的IP字符串 char ip[16] = ""; const char * str = inet_ntop(AF_INET, &num, ip, 16); printf("str : %s\n", str); printf("ip : %s\n", ip); printf("%d\n", ip == str); return 0; }
socket地址
通用 socket 地址
#include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; typedef unsigned short int sa_family_t;
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的
通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[ 128 - sizeof(__ss_align) ]; }; typedef unsigned short int sa_family_t;
专用 socket 地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现 在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; char sun_path[108]; };
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - }; struct in_addr { /* __SOCKADDR_COMMON(sin_) */ /* Port number. */ /* Internet address. */ sizeof (in_port_t) - sizeof (struct in_addr)]; in_addr_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; /* Transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef uint16_t in_port_t; typedef uint32_t in_addr_t; #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地 址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
socket相关函数
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字 - 参数: - domain: 协议族
AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型 SOCK_STREAM : 流式协议 SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 功能: 客户端连接服务器
- 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小 - 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据 ssize_t read(int fd, void *buf, size_t count); // 读数据