Linux网络IPC sockets
?
一、主机字节序和网络字节序
字节序分为大端和小端。
- 大端字节序:正序存储,一个整数的高位(23-31bit)存储在内存的低地址处,低位字节(0-7bit)储存在高地址。网络通讯通常使用大端字节序,也称为网络字节序。
- 小端字节序:逆序存储,一个整数的高位(23-31bit)存储在内存的高地址处,低位字节(0-7bit)储存在低地址。PC大多采用小端字节序,也称为主机字节序。
判断自己的电脑是大端还是小端代码:
#include <stdio.h>
union {
short value;
char ch[2];
}test;
int main()
{
test.value = 0x0102;
if (test.ch[0] == 1 && test.ch[1] == 2)
printf("本机是大端字节序\n");
else if (test.ch[0] == 2 && test.ch[1] == 1)
printf("本机是小端字节序\n");
else
printf("本机字节序未知\n");
}
?
n->网络,h->主机
Linux提供两种字节序之间的转换函数:
#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);
?
二、socket 地址格式
1、通用socket地址
不同格式的地址可以传递给套接字函数,被转换为通用的sockaddr 地址结构。
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
sa_data只有14字节,不够用时用下面这个新的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)];
};
?
2、专用socket地址
通用地址显然不好用,端口和IP需要复杂的位操作。 Linux为各个协议族提供了专门的socket地址结构体。
2.1、UNIX本地域协议族
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sa_family;
char sun_path[108];
};
?
2.2、TCP/IP协议族IPv4
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
?
2.3、TCP/IP协议族IPv6
#include <netinet/in.h>
struct in6_addr {
uint8_t s6_addr[16];
};
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
?
3、IP地址转换
inet_addr 和inet_ntoa 函数,用于在二进制地址格式和点分十进制(a.b.c.d)格式的字符串之间进行转换。这些函数仅适用于IPv4地址。
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
in_addr_t inet_addr(const char *cp);
要注意的是inet_ntoa是不可重入的,因为该函数内部用的是一个静态变量来储存转化结果,重复被赋值就不生效了,演示如下。
int main()
{
struct in_addr addr1, addr2;
inet_aton("1.2.3.4", &addr1);
inet_aton("11.22.33.44", &addr2);
char *sz_addr1 = inet_ntoa(addr1);
char *sz_addr2 = inet_ntoa(addr2);
printf("sz_addr1 = %s\n", sz_addr1);
printf("sz_addr2 = %s\n", sz_addr2);
}
# 输出结果
sz_addr1 = 11.22.33.44
sz_addr2 = 11.22.33.44
? 新函数inet_ntop 和inet_pton 支持类似的功能,并且可以同时使用IPv4和IPv6地址。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);
size 指定目标存储单元的大小,下面两个宏分别用于IPv4和IPv6。
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
?
三、创建socket
创建套接字,调用socket函数。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
SOCK_STREAM 流服务(常用于TCP)
SOCK_DGRAM 传输使用UDP协议
SOCK_RAW 数据报
内核2.6.17之后,支持与运算下面的值
SOCK_NONBLOCK 创建的sokcet为非阻塞的
SOCK_CLOEXEC 用fork创建子进程时在子进程中关闭该socket
?
四、服务端绑定socket地址
服务端必须将socket绑定到一个地址上,这个步骤也称为命名socket,客户端不需要绑定。 使用bind函数将地址与socket绑定起来。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind将addr 所指向的socket地址分配给为命名的sockfd 文件描述符,addrlen 参数表示该socket地址的长度。 返回值常见的errno是EACCES 和EADDRINUSE 。
- EACCES:被绑定的地址是受保护的地址,仅超级用户能访问,比如绑定到(0-1024)时。
- EADDRINUSE:被绑定的地址正在使用中,比如绑定到一个处于TIME_WAIT状态的socket地址时。
?
五、服务端监听socket
socket绑定地址命名后,还不能接受客户端的链接,需要创建一个监听队列存放待处理的客户链接:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog 参数提示内核监听队列的最大长度。若监听队列的长度超过该值,服务器不再受理新客户的链接,客户端将受到ECONNREFUSED错误信息。
?
六、服务端接受连接
一旦服务器调用了listen,所使用的套接字就可以接收连接请求。 我们使用accept函数来检索连接请求并将其转换为连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
由accept返回的文件描述符是一个连接到调用connect的客户端的套接字描述符。 这个新的套接字描述符具有与原始套接字(sockfd)相同的套接字类型和地址族。 传递给accept的原始套接字与连接不相关,但仍然可用来接收其他连接请求。 如果不需要关心客户端的标识,可以将addr和len参数设置为NULL。 否则,在调用accept之前,需要将addr参数设置为一个足够大的缓冲区来保存地址,并将len所指向的整数设置为缓冲区的大小(以字节为单位)。 在返回时,accept将在缓冲区中填写客户端的地址,并更新len所指向的整数以反映地址的大小。
?
七、客户端发出连接
客户端使用connect函数来创建连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
|