第一章、网络编程基础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; /* address family */
char sa_data[14]; /* variable-length address */
};
- sa_family :协议族(地址族)
- sa_data: 存放地址数据
sa_data只有14字节,不够用时用下面这个新的socket通用地址结构体:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; /* address family */
unsigned long int __ss_align; /* Force desired alignment. */
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; /* address family */
char sun_path[108]; /* Path name. */
};
2.2、TCP/IP协议族IPv4
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr; /* IPv4 address, Network byte order */
};
struct sockaddr_in {
sa_family_t sin_family; /* address family */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* IPv4 address */
};
2.3、TCP/IP协议族IPv6
#include <netinet/in.h>
struct in6_addr {
uint8_t s6_addr[16]; /* IPv6 address, Network byte order */
};
struct sockaddr_in6 {
sa_family_t sin6_family; /* address family */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* traffic class and flow info */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* set of interfaces for scope */
};
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_aton,但要谨慎使用,-1(255.255.255.255)时无效地址有问题。
要注意的是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);
// inet_pton() 在成功时返回1(网络地址成功转换)。 如果SRC中不包含表示指定地址族中合法网络地址的字符串,则返回0。 如果af不包含有效的地址族,则返回-1,并将errno设置为EAFNOSUPPORT。
inet_pton() 在成功时返回1(网络地址成功转换)。 如果SRC中不包含表示指定地址族中合法网络地址的字符串,则返回0。 如果af不包含有效的地址族,则返回-1,并将errno设置为EAFNOSUPPORT。
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);
// Returns: file (socket) descriptor if OK, −1 on error
- domain :域常用参数是AF_UNIX(AF_LOCAL)、AF_INET、AF_INET6。
- type :
SOCK_STREAM 流服务(常用于TCP)
SOCK_DGRAM 传输使用UDP协议
SOCK_RAW 数据报
内核2.6.17之后,支持与运算下面的值
SOCK_NONBLOCK 创建的sokcet为非阻塞的
SOCK_CLOEXEC 用fork创建子进程时在子进程中关闭该socket
- protocol :在前两个参数的基础上指定一个具体的协议,通常为0。
四、服务端绑定bind
服务端必须将socket绑定到一个地址上,这个步骤也称为命名socket,客户端不需要绑定。
使用bind函数将地址与socket绑定起来。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error
bind将addr
所指向的socket地址分配给为命名的sockfd
文件描述符,addrlen
参数表示该socket地址的长度。
返回值常见的errno是EACCES
和EADDRINUSE
。
- EACCES:被绑定的地址是受保护的地址,仅超级用户能访问,比如绑定到(0-1024)时。
- EADDRINUSE:被绑定的地址正在使用中,比如绑定到一个处于TIME_WAIT状态的socket地址时。
对于Internet域,如果指定特殊的IP地址INADDR_ANY(定义在<netinet/in.h>),socket端点将被绑定到系统的所有网络接口。这意味着可以从系统中安装的任何NIC网卡接收数据包。
可以使用getsockname函数来发现绑定到套接字的地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict alenp);
// Returns: 0 if OK, −1 on error
在调用getsockname之前,设置alenp指向一个包含sockaddr缓冲区大小的整数。在返回时,该整数被设置为返回的地址的大小。如果地址不能装入所提供的缓冲区,地址将被静默地截断。如果当前没有绑定到套接字的地址,则没有定义结果。
如果套接字连接了peer,可以通过调用getpeername函数来找到peer的地址。
除了返回对等体的地址之外,getpeername函数与getsockname函数是相同的。
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict alenp);
// Returns: 0 if OK, −1 on error
五、服务端监听listen
socket绑定地址命名后,还不能接受客户端的链接,需要创建一个监听队列存放待处理的客户链接:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// Returns: 0 if OK, −1 on error
backlog
参数提示内核监听队列的最大长度。若监听队列的长度超过该值,服务器不再受理新客户的链接,客户端将受到ECONNREFUSED错误信息。
下面显示初始化一个socket的接口,返回一个sockfd给服务端使用。
#include <errno.h>
#include <sys/socket.h>
int init_socket(int type, const struct sockaddr *addr, socklen_t alen,
int qlen)
{
int fd;
int err = 0;
if ((fd = socket(addr->sa_family, type, 0)) < 0)
return(-1);
if (bind(fd, addr, alen) < 0)
goto errout;
if (type == SOCK_STREAM || type == SOCK_SEQPACKET) {
if (listen(fd, qlen) < 0)
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(-1);
}
六、服务端接受连接accept
一旦服务器调用了listen,所使用的套接字就可以接收连接请求。 我们使用accept函数来检索连接请求并将其转换为连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
// Returns: file (socket) descriptor if OK, −1 on error
如果没有连接请求等待,accept将阻塞,直到有一个请求到达。如果sockfd处于非阻塞模式,accept将返回−1,并将errno设置为EAGAIN或
EWOULDBLOCK。
由accept返回的文件描述符是一个连接到调用connect的客户端的套接字描述符。 这个新的套接字描述符具有与原始套接字(sockfd)相同的套接字类型和地址族。 传递给accept的原始套接字与连接不相关,但仍然可用来接收其他连接请求。
如果不需要关心客户端的标识,可以将addr和len参数设置为NULL。 否则,在调用accept之前,需要将addr参数设置为一个足够大的缓冲区来保存地址,并将len所指向的整数设置为缓冲区的大小(以字节为单位)。 在返回时,accept将在缓冲区中填写客户端的地址,并更新len所指向的整数以反映地址的大小。
连接案列:
七、客户端发出连接connect
客户端使用connect函数来创建连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
// Returns: 0 if OK, −1 on error
使用connect指定的地址是希望与之通信的服务器的地址。如果sockfd没有绑定到一个地址,connect将为调用者绑定一个默认地址。
当尝试连接到服务器时,连接请求可能会因为几个原因而失败。为了连接请求成功,试图连接的机器必须启动并运行,服务器必须绑定到试图联系的地址,并且服务器的挂起连接队列中必须有空间。因此,应用程序必须能够处理可能由瞬态条件引起的连接错误返回。
下面是使用指数后退算法处理临时连接错误的方法。这些错误很可能发生在服务器运行在负载沉重的系统上。
#include <sys/socket.h>
#define MAXSLEEP 128
int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen)
{
int numsec;
/* 指数后退算法。如果连接调用失败,进程会休眠一段时间。
然后再次尝试,每次循环都会增加延迟,最大延迟约为2分钟。*/
for (numsec = 1; numsec <= MAXSLEEP; numsec <<= 1) {
if (connect(sockfd, addr, alen) == 0) {
return(0);
}
// 延迟重连
if (numsec <= MAXSLEEP/2)
sleep(numsec);
}
return(-1);
}
这种算法只能运行在linux上,不可移植FreeBSD,可移植版参考UNIX高级环境编程。
八、关闭连接close & shutdown
打开的文件通过调用close函数来关闭,这一点在多线程编程中尤其要注意,如果忘记了关闭,当fd的引用计数超过1024后,文件将拒绝访问,这样就造成了句柄泄露。
#include <unistd.h>
int close(int fd);
// Returns: 0 if OK, −1 on error
关闭文件还会释放进程对文件可能拥有的任何记录锁。
close并非总是立即关闭一个链接,而是将文件描述符fd的引用计数减1,只有当fd引用计数为0时,才真正的关闭链接。多进程程序中,一次fork后会将父进程中打开的socket引用计数加1,因此必须在父进程和子进程中都对该socket执行close才能关闭链接。
若一定要立即终止链接,可以直接使用shutdown
调用。socket上的通信是双向的,可以使用shutdown功能在socket上禁用I/O。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
// Returns: 0 if OK, −1 on error
如果how
是SHUT_RD
,那么socket读取被禁用。
如果how
是SHUT_WR
,那么socket写入被禁用。
如果how
是SHUT_RDWR
,同时禁用socket读取和写入。
九、套接字数据传输
1、TCP数据读写
对文件的读写read和write同样使用socket,但socket提供了6个专用接口函数,增强了对socket数据的读写。
TCP读写系统调用如下:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// Returns: number of bytes sent if OK, −1 on error
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// Returns: length of message in bytes,
// 0 if no messages are available and peer has done an orderly shutdown,
// or −1 on error
1、send试图发送一个大于协议支持的最大消息,则send将失败,errno设置为EMSGSIZE。使用字节流协议,send将阻塞,直到传输完全部数据。send主要关注flags
,如下图。
2、recv的flags
参数通常设置为0,详情见下图。recv成功时读取的长度可能小于期望的长度len,因此需要多次调用recv,才能读取到完整的数据。recv返回0,意味着通信对方已经关闭连接了。
flags参数如下:
发送紧急数据会截断普通数据,案列如下,首先是客户端
int main(int argc, char *argv[])
{
if (argc <= 2)
{
DBG_PRINT("main parameter error");
return -1;
}
// 打印运行程序名
DBG_PRINT("parameter right, run procedure %s", basename(argv[0]));
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(addr)) < 0)
printf("connetc failed\n");
else {
const char* oob_data = "abcd"; // 带外数据(紧急,快速数据)
const char* normal_data = "1234"; // 普通数据
send(sockfd, normal_data, strlen(normal_data), 0);
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
send(sockfd, normal_data, strlen(normal_data), 0);
}
close(sockfd);
}
服务端:
int main(int argc, char *argv[])
{
if (argc <= 2)
{
DBG_PRINT("main parameter error");
return -1;
}
// 打印运行程序名
DBG_PRINT("parameter right, run procedure %s", basename(argv[0]));
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (const struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 5);
struct sockaddr_in client;
socklen_t client_addr_len = sizeof(client);
int connfd = accept(sockfd, (struct sockaddr *)&addr, &client_addr_len);
if (connfd < 0) {
DBG_PRINT("error is %d", errno);
}
else {
char buff[1024] = {};
int ret = recv(connfd, buff, 1024 - 1, 0);
printf("got %d bytes of normal data: %s\n", ret, buff);
bzero(buff, 1024);
ret = recv(connfd, buff, 1024 - 1, MSG_OOB);
printf("got %d bytes of oob data: %s\n", ret, buff);
bzero(buff, 1024);
ret = recv(connfd, buff, 1024 - 1, 0);
printf("got %d bytes of normal data: %s\n", ret, buff);
close(connfd);
}
close(sockfd);
}
# 如果连续发送3个普通数据,服务端打印结果应该是这样的
got 8 bytes of normal data: 1234abcd1234
got 0 bytes of normal data:
got 0 bytes of normal data:
# 本案列运行后,服务端打印如下数据,d就是OOB,截断了正常数据的收发
got 7 bytes of normal data: 1234abc
got 1 bytes of oob data: d
got 4 bytes of normal data: 1234
&esmp;
2、UDP数据读写
UDP读写系统调用如下:
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict addr, socklen_t *restrict addrlen);
// Returns: length of message in bytes,
// 0 if no messages are available and peer has done an orderly shutdown,
// or −1 on error
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen);
//Returns: number of bytes sent if OK, −1 on error
&esmp;
3、TCP和UDP通用的数据读写
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
// Returns: length of message in bytes,
// 0 if no messages are available and peer has done an orderly shutdown,
// or −1 on error
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
// Returns: number of bytes sent if OK, −1 on error
九、socket选项 (重点)
以下函数用于获取和设置socket文件描述符属性:
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict lenp);
int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
// Returns: 0 if OK, −1 on error
level
要操作的是什么协议,socket通常是SOL_SOCKET
option
选项,如下表
val 和 len
是该option选项对应的值,和该值对应的长度,如下表
1、SO_REUSEADDR 重用本地地址
当服务器终止时,尝试立即重新启动它。通常,TCP的实现会阻止绑定相同的地址,直到超时过期(TIME_WAIT),这通常是几分钟的时间。幸运的是,SO_REUSEADDR套接字选项可以绕过这个限制。
注意服务端端要在监听之前设置,某些选项客户端要在connect之前设置,以下是代码清单。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int reuse = 1;
// 在bind和listen之前设置
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))
bind(sockfd, (const struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 5);
2、SO_RCVLOWAT和SO_SNDLOWAT
它们一般被I/O复用函数用来判断socket是否可读可写。
第二章、I/O复用
当我们从一个描述符读取并写入到另一个描述符时,我们可以在循环中使用阻塞I/O,例如
while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");
我们一次又一次地看到这种阻塞I/O的形式。 如果我们需要读两个描述符呢? 在这种情况下,我们不能在任何一个描述符上进行阻塞读取,因为数据可能出现在一个描述符上,而我们在另一个描述符上的读取被阻塞。 处理这种情况需要一种不同的技术 。
更好的技术是使用I/O多路复用。 为此,我们构建一个感兴趣的描述符列表(通常不止一个描述符),并调用一个函数,直到其中一个描述符准备好进行I/O时才返回。 三个函数-poll、pselect和select -允许我们执行I/O多路复用。 从这些函数返回时,我们被告知哪些描述符已经准备好进行I/O。
一、select
传递给select的参数告诉内核我们感兴趣的描述符。
- 我们对每个描述符感兴趣的条件是什么。 (我们想要从给定的描述符中读取吗? 我们要写入给定的描述符吗? 我们是否对给定描述符的异常条件感兴趣?)
- 我们想等多久。 (我们可以永远等待,等待固定的时间,或者根本不等待。) 在select返回时,内核告诉我们。
- 已经准备好的描述符的总数。
- 哪些描述符可以满足这三种条件(读、写或异常条件)
在select返回的时候,内核告诉我们已经准备好的描述符的总数;
有了这些返回信息,我们可以调用相应的I/O函数(通常是读或写),并且知道函数不会阻塞。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// Returns: count of ready descriptors, 0 on timeout, −1 on error
nfds:指定被监听的文件描述符的总数,通常是connfd+1。
readfds, writefds, exceptfds:描述符可以满足这三种条件(读、写或异常条件)
使用如下宏操作fd_set 结构体的位:
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *set); // 测试fdset的位fd是否被设置
// Returns: nonzero if fd is in set, 0 otherwise
void FD_CLR(int fd, fd_set *set); // 清除fdset的位fd
void FD_SET(int fd, fd_set *set); // 设置fdset的位fd
void FD_ZERO(fd_set *set); // 清除fdset的所有位
timeout:指定超时时间,timeval 结构体如下:
struct timeval
{
__time_t tv_sec; /* 秒 Seconds. */
__suseconds_t tv_usec; /* 微秒 Microseconds. */
};
timeout=NULL:select将一直阻塞,直到某个文件描述符就绪。
tv_sec0 || tv_usec=0:select立即返回。
select在等待期间,若程序接受到信号,则select立即返回-1,error为EINTR
以下是一个select用法小案列:
int main(int argc, char *argv[])
{
if (argc <= 2)
{
DBG_PRINT("main parameter error");
return -1;
}
// 打印运行程序名
DBG_PRINT("parameter right, run procedure %s", basename(argv[0]));
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (const struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 5);
struct sockaddr_in client;
socklen_t client_addr_len = sizeof(client);
int connfd = accept(sockfd, (struct sockaddr *)&addr, &client_addr_len);
if (connfd < 0) {
DBG_PRINT("error is %d", errno);
close(sockfd);
}
char buf[1024] = {};
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(read_fds);
FD_ZERO(exception_fds);
int ret = -1;
while (1) {
memset(buf, 0, sizeof(buf));
// 每次调用select前都要重新在fd_set变量中设置文件描述符connfd
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
if (ret < 0)
break;
// 对于可读事件,普通的recv
if (FD_ISSET(connfd, &read_fds)) {
ret = recv(connfd, buf, sizeof(buf) - 1, 0);
if (ret < 0)
break;
}
// 对于异常事件,采用带MSG_OOB标志的recv
else if (FD_ISSET(connfd, &read_fds)) {
ret = recv(connfd, buf, sizeof(buf) - 1, SG_OOB);
if (ret < 0)
break;
}
}
// 释放句柄
close(connfd);
close(sockfd);
}