大端字节序(big endian):高位字节放在低地址。
小端字节序(little endian):高位字节放在高地址。
可使用如下代码检测机器的字节序:
void byteorder()
{
union
{
short value;
char union_bytes[sizeof( short )];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)) printf("big endian\n");
else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1)) printf("little endian\n");
else printf("unknown\n");
}
由于现代 PC 多采用小端字节序,所以小端字节序又被称为主机字节序。(JAVA 虚拟机采用大端字节序),而在网络中规定所有数据采用大端字节序的方式传输,所以大端字节序又叫做网络字节序。
正是由于上述原因,所以在进行数据传输时需要在小端字节序和大端字节序之间进行转换。Linux 有如下 4 个函数进行转换:
#include
unsigned long int htonl(unsigned long int hostlong);//"host to network long"
unsigned short int htons (unsigned short int hostshort);//"host to network short"
unsigned long int ntohl (unsigned long int netlong);//"network to host long"
unsigned short int ntohs (unsigned short int netshort);//"network to host short"
通用 socket 地址在 Linux 头文件
中,但是通过通用 socket 地址难以获取 IP 地址和端口号,所以 Linux 还为各个协议族提供了专门的 socket 地址结构体。但是在实际使用过程中,所有的 socket 编程接口都使用 sockaddr (即通用 socket 地址),我们只需要强制转换即可。
老版本的点分十进制的 IPv4 地址和网络字节序整数的转换函数:
#include
in_addr_t inet_addr (const char* strptr);
int inet_aton (const char* cp, struct in_addr* inp);
char* inet_ntoa (struct in_addr in);
第一个将点分十进制 IPv4 地址字符串转换为网络字节序整数。(失败返回 INADDR_NONE)
第二个与第一个相同,不过把结果存储在 inp 结构中。(成功返回 1 ,失败返回 0 )
第三个函数将网络字节序整数表示的 IPv4 地址转换为点分十进制表示。(其存储在一个静态变量内存内,返回时也是返回其地址,所以其是不可重入的,自然也没有线程安全性)
新版本的同时支持 IPv4 和 IPv6 的地址转换函数:
#include
int inet_pton (int af, const char* src, void* dst);
const char* inet_ntop (int af, const void* src, char* dst, socklen_t cnt);
第一个函数将字符串表示形式的 IP 地址 src 转换成网络字节序整数表示的 IP 地址并存储在 dst 中(af 指定地址族,如 AF_INET 或 AF_INET6),成功时返回 1, 失败时返回 0 并设置 errno。
第二个函数将进行相反变换,最后一个参数表示指定存储单元的大小。(如系统中宏定义的 IPv4 大小为 16, IPv6 大小为 46)
#include
#define INET_ADDRSTRLEN 16;
#define INET6_ADDRSTRLEN 46;
socket 也是 Linux 文件哲学中的一部分,也是可读可写、可开可关的文件描述符。创建 socket 的 API 如下:
#include
#include
int socket (int domain, int type, int protocol);
其中,domain 参数表示使用哪个底层协议族(PF_INET 为 IPv4、PF_INET6 为 IPv6、PF_UNIX 为 UNIX 本地协议族);type 参数指定服务类型(对 TCP/IP 协议族而言,SOCK_STREAM 流服务表示 TCP 协议、SOCK_UGRAM 数据报服务表示 UDP 协议。注意,在 Linux 2.6.17后 type 参数可以与 SOCK_NONBLOCK (设为非阻塞)和 SOCK_CLOEXEC (fork创建子进程时关闭该 socket)进行与运算从而达到响相应目的);protocol 参数表示在前面两个参数集合之下再选择一个具体的协议,通常设置为 0 表示使用默认协议。
socket 调用成功时返回 socket 文件描述符,否则返回 -1 并设置 errno。
在服务器程序中只有命名了 socket 后客户端才能知道如何连接它;对应的是客户端通常不需要命名 socket ,而是直接采用匿名的方式(操作系统自行分配的地址)进行访问。命名 socket
的系统调用如下:
#include
#include
int bind (int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
上述函数将 my_addr 所指的 socket 地址分配给未命名的 sockfd 文件描述符, addrlen 指明该 socket 地址的长度。
调用成功时返回 0, 否则返回 -1 并设置 errno。其中常见的两种 errno 是 EACCES (绑定的地址是受保护的地址,例如普通用户将其绑定到知名服务端口 0 ~ 1023)和 EADDRINUSE (绑定的地址正在使用中,例如将地址绑定到一个处于 TIME_WAIT 状态的 socket 地址)
socket 被命名之后还需要创建一个监听队列来存放待处理的客户连接。
#include
int listen (int sockfd, int backlog);
sockfd 参数指定被监听的 socket ;backlog 指示内核监听队列的最大长度,当监听队列长度大于其时,服务端不再受理新的客户连接(客户端将收到 ECONNREFUSED 错误)。
调用成功时返回 0, 否则返回 -1 并设置 errno。
下面的系统调用从 listen 监听队列中接受一个连接:
#include
#include
int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
sockfd 参数是执行监听的 socket;addr 参数用于获取发起连接的远端 socket 地址,其长度由 addrlen 指出。
执行失败时返回 -1, 并设置 errno。
注意:accept 函数从监听队列中取出 socket 连接时不关心其连接状态,故若其连接状态已经是 ESTABLISHED 或者 CLOSE_WAIT 都不影响 accept 函数的"正确"返回。
客户端通过下面的系统调用发起连接。
#include
#include
int connect (int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
sockfd 参数是用于建立连接的 socket 文件描述符;serv_addr 是服务器的监听 socket 地址, addrlen 指出其长度。
执行成功返回 0,失败时返回 -1,并设置 errno(ECONNREFUSED 表示目标端口不存在,连接被拒绝;ETIMEDOUT 表示连接超时)。
可以通过 close (int fd) 的方式关闭 socket 文件描述符。当然,如果有父子进程复制了此文件描述符,close 只会使其引用计数 -1 (减到 0 时才会真正关闭)。
所以,我们可以使用下面的调用立即关闭 socket 。
#include
int shutdown(int sockfd, int howto);
其中 howto 参数决定关闭的形式:
SHUT_RD 表示关闭读;
SHUT_WR 表示关闭写;
SHUT_RDWR 表示关闭读写;
同理, 成功时返回 0,失败时返回 -1 并设置 errno。
通常的 read 和 write 函数对 socket 同样有效(本质上是文件),不过为了加强读写控制,就有了专门使用的系统调用。
TCP 流数据的相关读写系统调用:
#include
#include
ssize_t recv (int sockfd, void* buf, size_t len, int flags);
ssize_t send (int sockfd, const void* buf, size_t len, int flags);
recv 读取 sockfd 上的数据,buf 和 len 指出读缓冲区的位置和大小,flags 提供额外的功能(如 MSG_OOB 表示发送或接受紧急数据(带外数据));返回成功读取到的数据长度,返回 0 意味着对方关闭了连接,出错时返回 -1 并设置 errno。
send 往 sockfd 上写入数据,buf 和 len 指出写缓冲区的位置和大小;返回成功写入的数据长度,失败返回 -1 并设置 errno。
UDP 数据报的相关读写系统调用:
#include
#include
ssize_t recvfrom (int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto (int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
recvfrom 读取 sockfd 上的数据,buf 和 len 指出读缓冲区的位置和大小,由于 UDP 没有连接概念,所以我们每次都要指明发送端的 socket 地址。返回值意义同上。
sendto 往 sockfd 上写入数据,buf 和 len 指出写缓冲区的位置和大小;返回值意义同上。
这两个读写调用也可用于面向连接的情况,只需将后面两个参数置为 NULL。
#include
ssize_t recvmsg (int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg (int sockfd, struct msghdr* msg, int flags);
其中 msghdr 结构体存储了 socket 地址、辅助数据、flags 等等(同样对于面向连接的协议应该被置为 NULL)。
#include
int sockatmark (int sockfd);
此调用判断 sockfd 是否处于带外标记内(下一个读取的数据是否是带外数据),如果是其返回 1(再用带 MSG_OOB 的读函数读取),否则返回 0。
#include
int getsockname (int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername (int sockfd, struct sockaddr* address, socklen_t* address_len);
getsockname 获取 sockfd 对应的本端 socket 地址并将其存储在 address 中。成功时返回 0,失败返回 -1 并设置 errno。
getpeername 获取 sockfd 对应的远端 socket 地址并将其存储在 address 中。返回值同上。
设置 socket 文件描述符的一些属性。
#include
int getsockopt (int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt (int sockfd, int level, int option_name, const void* option_value, socklen_t* option_len);
相关参数说明如下:(成功返回0,失败返回-1并设置errno)
注意由于 TCP 三次握手要建立交换一些初始信息,所以一些 socket 属性要在其之前进行设置。
#include
struct hostent* gethostbyname (const char* name);
struct hostent* gethostbyaddr (const void* addr, size_t, int type);
前者通过主机名获取主机完整信息;后者通过 IP 地址获取主机完整信息。
#include
struct servent* getservbyname (const char* name, const char* proto);
struct servent* getservbyport (int port, const char* proto)
前者通过名称获取服务的完整信息;后者通过端口号获取服务的完整信息。其中 proto 指明服务类型(通常为 TCP 或 UDP)。
注:上述四个函数都是非线程安全的,线程安全的函数版本在其后加 ‘_r’ 。
这是我自己整理的学习笔记,主要用于自我复习。如果有大佬也看到了这个并且发现了谬误,欢迎email me at [email protected]
。