简述和TCP/IP
如果要写一个简单的ipv6客户端程序,需要sockaddr_in6地址,其余用法和ipv4的用法一致,
先指定地址族,端口,ip地址,connect成功后,就可以发包收包
int inet_pton(int af, const char *restrict src, void *restrict dst);
其中af可以是ipv4,也可以是ipv6,此函数的目的是把字符串转换为dst二进制流
如果加了锁 pthread_cond_wait 会保证 比 pthread_cond_signal 先解锁
32位系统中size_t是一个32位值, 64系统中size_t是一个64位值
UDP不保证各个数据报顺序跨网络后保持不变,也不保证每个数据报只到达一次
传输层
UDP是无连接的,以下的代码是客户端给服务端进行发送时的处理, 其中dest_addr可以动态的切换
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
SCTP 在客户端与服务器之间提供关联(一个关联是指两个系统间的一次通信),可能因为SCTP支持多宿(单个SCTP结点能够支持多个ip地址)而涉及不止两个地址
SCTP是面向消息的
TIME_WAIT的时间一般是最长分结生命期(MSL)的两倍,通常为1分钟~4分钟
此图比较仔细的说明了,客户端与服务端三次握手,数据通信,四次分手的过程以及调用的函数,其中我们比较关注TIME_WAIT状态,可以看出,四次分手时,当服务端准备好断开时,会主动发送一个FIN,这时客户端处于TIME_WAIT状态
TIME_WAIT解决方式可以使用socket opt选项进行复用,并且TIME_WAIT的状态有两个存在的理由
- 可靠的实现TCP全双工连接的终止
当服务端ACK M + 1丢失,那么客户端必须要维护状态信息,当服务端发送FIN N时,要提醒服务端重新发送ACK - 允许老的重复分节在网络中消逝
当老的连接和端口的链路断开后,过一段时间相同的连接和端口建立另外一个链接,TCP必须保证老连接的数据重复在新链路发送,既然TIME_WAIT的时间是MSL(MSL,即Maximum Segment Lifetime,一个数据分片(报文)在网络中能够生存的最长时间)的两倍,那么足以让老的分组数组只存货MSL时间后就被丢弃
众所周知的端口号范围是0 ~ 1023, 由LANA(the internet assigned numbers authority)
1024 ~ 49154 不受LANA控制,有LANA控制并维护使用状况清单
49152 ~ 65535是动态的或私用的端口,这些端口我们称之临时端口
如果一个服务器的地址是多宿的,我们想让任何一个地址都可以建立连接,那么我们可以指定服务器监听的地址为INADDR_ANY,
ipv4最大的数据报为65535个字节,包括IPV4首部20和tcp首部20,ipv6数据报的最大大小为65575个字节,包括60字节的ipv6首部,
ipv4要求的最小链路MTU是68字节,允许最大的IPV4头部(40字节)拼接最小的片段,ipv6要求的最小MTU为1280字节
ipv4和ipv6都定义了最小重组缓冲区的大小,ipv4的默认缓冲区是576字节,对于ipfv6是1500字节
ipv4首部DF位如果被设置,则代表不分片
quinta@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 6291456
quinta@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
quinta@ubuntu:~$ cat /proc/sys/net/core/rmem_max
212992
quinta@ubuntu:~$ cat /proc/sys/net/core/wmem_max
212992
从上到下,分别是tcp 默认读缓冲区,tcp默认写缓冲区,tcp可设置读最大缓冲区,tcp可设置写最大缓冲区
如果UDP发送的大小大于buffer的长度,那么可能会报错EMSGSIZE,也可能不会报错,但可能有的UDP不返回这种错误。
socketadd_in和 socketaddr_un可以无缝和socketaddr进行转换,都是16字节,那么我们为什么要使用socketaddr_in进行赋值呢,一i那位socketaddr中端口和ip是在一个数组中,对开发者不友好
套接字编程简介
IPV6的地址族是AF_INET6,
IPV6 新定义了通用字段 sockaddr_storage, sockaddr_storage足够大,可以容纳系统支持的最苛刻的对齐要求。
include <netinet/in.h>
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
sockaddr_in 占用20字节
// IPv4 AF_INET sockets:
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
sockaddr_in6 占用28字节
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
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 */
};
struct in6_addr {
union {
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
sockaddr_storage占用128字节
#define _SS_MAXSIZE 128 /* Implementation specific max size */
#define _SS_ALIGNSIZE (sizeof (int64_t))
/* Implementation specific desired alignment */
/*
* DeFinitions used for sockaddr_storage structure paddings design.
*/
#define _SS_PAD1SIZE (_SS_ALIGNSIZE - sizeof (sa_family_t))
#define _SS_PAD2SIZE (_SS_MAXSIZE - (sizeof (sa_family_t)+
_SS_PAD1SIZE + _SS_ALIGNSIZE))
struct sockaddr_storage {
sa_family_t __ss_family; /* address family */
/* Following fields are implementation specific */
char __ss_pad1[_SS_PAD1SIZE];
/* 6 byte pad,this is to make implementation
/* specific pad up to alignment field that */
/* follows explicit in the data structure */
int64_t __ss_align; /* field to force desired structure */
/* storage alignment */
char __ss_pad2[_SS_PAD2SIZE];
/* 112 byte pad to achieve desired size,*/
/* _SS_MAXSIZE value minus size of ss_family */
/* __ss_pad1,__ss_align fields is 112 */
};
sockaddr_un是UNIX域,
从进程到内核传递套接字地址结构的函数有三个,bind connect sendto
从内核到进程的函数有四个,accept recvfrom getsockname getpeername,
大端地址 是 高位字节 在 低地址,小端地址则相反
以下是一段验证大小端的代码和htons 和 ntohs的博客,通过结果可以看出htons和ntohs的转换结果一致,说明了两个函数的作用一致,如果是大端,则转化为小端,如果是小端,则转换为大端
#include<stdio.h>
#include <arpa/inet.h>
int main()
{
uint16_t num = 4096; // 0x1000
if(num == htons(num))
{
printf("big endian\n");
printf("address = %X\n", num);
}
else
{
printf("little endian");
printf("address = %X\n", num);
}
uint16_t big_num = htons(num);
printf("num = %d\n", big_num);
printf("address = %X\n", big_num);
big_num = ntohs(num);
printf("num = %d\n", big_num);
printf("address = %X\n", big_num);
return 0;
}
inet_aton 用来将ip地址转换为32位的网络序
inet_addr有类似的作用,这个函数现在已经废弃不用
inet_ntoa用来将in_addr结构转换为字符串
随着ipv6的发展,新增了有类似功能的函数
其中family可以显示的指定网络协议名称,AF_INET或AF_INET6, inet_pton用来将ip地址转换为网络序,inet_ntop转换,可以理解为inet_pton和inet_aton是套了层壳子
基本TCP套接字编程
客户端 connect函数前不需要调用bind,这时连接时内核会分配一个临时端口
listen函数把一个未连接的套接字转换为一个被动套接字,指示内核应该接受该套接字的连接请求
int listen(int sockfd, int backlog);其中backlog解释如下,简单可理解为服务器可支持的最大连接数
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);可以看到addr是指对端的ip地址
The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket
fork如果在accept之后,那么文件描述符,父进程与子进程共享,需要将父进程的文件描述符关闭,如果想要彻底关闭文件描述符,那么需要在父进程,子进程都调用close来关闭
在一个没有调用bind的TCP客户端,connect成功返回后,getsocketname返回的是内核赋予该连接的本地ip地址和本地端口号
在以0去bind的时候,getsockname返回的是内核赋予的本地端口号,同样也可以获得某个套接字的地址族,适合于以下情况,未调用bind就connect了
TCP客户/服务端程序示例
SIGCHLD 是内核在任何一个进程停止时,发给它的父进程的一个信号,如果父进程比子进程先结束,而且没有回收子进程,那么子进程将会变成一个僵尸进程
void (*signal(int sig, void (*func)(int)))(int)
图片部分来自于
https://www.runoob.com/cprogramming/c-function-signal.html
EINTER错误由在非阻塞套接字上不能立即完成的操作返回,例如,当套接字上没有排队数据可读时调用了recv()函数。此错误不是严重错误,相应操作应该稍后重试
wait函数会阻塞直到第一个子进程返回
wait_pid可以按子进程的进程号进行等待,正确的方法是在信号处理函数中循环调用waitpid进行处理
accept需要判断正确错误,如果返回一个非致命的错误,要再次进行accept,出现这种问题的原因是三次握手后,客户端又发送了一次复位的请求,内核队列中将会把此套接字删除,但是业务层调用accept时,不知道曾经有一个已完成的连接被从队列中删除了
当服务器进程停止时,会向客户端发送一个FIN,客户端回送一个ACK,此时服务器的状态时FIN_WAIT2, 客户端的状态是CLOSE_WAIT,但是此时客户端还是可以发送数据的,发送数据不会报错,服务器收到数据时会发送回一个响应,但是业务层不会收到响应,再次发送时会引用SIGPIPE信号,默认导致进程退出
假如客户端和服务器之间通过路由连接,但是服务器异常崩溃,那么tcp会重传数据约12次,直到放弃重传,客户机阻塞在readline中,最后会收到一个ETIMEOUT错误
客户端与服务器通信如果是文本串的格式,相对安全,如果是二进制结构,那么可能存在大小端转换的问题
I/O复用: select 和 poll函数
select和poll和组合的区别是多路复用可以预先告诉进程,使得内核一旦发现进程指定的一个或多个I/O条件就绪,就主动通知进程
异步接收数据时,如果数据没有准备好,那么会返回一个EWOULDBLOCK错误,如果数据复制了一些,那么此时会返回成功
接收数据也可以使用信号驱动模式,通讯过程如下
异步IO aio_read 和信号驱动模式的主要区别在于,信号驱动式I/O是由内核通知我们何时能启动一个I/O操作,而异步IO模型是由内核通知我们I/O操作何时完成
select 可以设置低水位,比如我们默认知道小于X字节时,不是有效的数据,则可以设置内核收到X字节时,才通知应用层
close函数把套接字的引用计数减1,当套接字等于0时,才会主动执行关闭操作,但是shutdown函数可以不管引用计数就激发TCP的正常连接终止序列
shutdown 依赖于 howto的值,SHUT_RD 关闭连接的读这一半,SHUT_WR关闭连接写这一半,SHUT_RDWR连接的读半部和写半部都关闭
for ( ; ; ) {
36 rset = allset; /* structure assignment */
37 nready = Select(maxfd+1, &rset, NULL, NULL, NULL);
38
39 if (FD_ISSET(listenfd, &rset)) { /* new client connection */
40 clilen = sizeof(cliaddr);
41 connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
42 #ifdef NOTDEF
43 printf("new client: %s, port %d\n",
44 Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
45 ntohs(cliaddr.sin_port));
46 #endif
47
48 for (i = 0; i < FD_SETSIZE; i++)
49 if (client[i] < 0) {
50 client[i] = connfd; /* save descriptor */
51 break;
52 }
53 if (i == FD_SETSIZE)
54 err_quit("too many clients");
55
56 FD_SET(connfd, &allset); /* add new descriptor to set */
57 if (connfd > maxfd)
58 maxfd = connfd; /* for select */
59 if (i > maxi)
60 maxi = i; /* max index in client[] array */
61
62 if (--nready <= 0)
63 continue; /* no more readable descriptors */
64 }
可以看下以上代码块,为什么每次用allset给rset复制,因为每次select时候,如果fd没有变更,那么select 会自动把fd从set中删除
当服务器使用阻塞IO时,可能会有以下问题,客户端只发送了一个字节后就不再发送,这时服务器下次就会在io处阻塞,影响其他用户的发送
如果不在关心某个特定描述符,那么可以把对应pollfd结构中的fd设置为一个负数
套接字选项
主要又四个函数需要着重了解
gesocketopt
setsocketopt
fcntl
ioctl
以上是所有的套接字选项
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
此时返回的optval是一个整数,如果为0,则代表相应选项被禁止,非0代表被启用
类似的,setsockopt也是如此,
tcp存在流量控制,不允许发送超过窗口大小的数据
TCP套接字缓冲区的长度至少是MSS的四倍
基本UDP套接字编程
UDP发送一个0字节长度是可行的,接受为0也不代表断开连接
大部分tcp服务器都是并发的,大部分udp服务器都是迭代的
#include "unp.h"
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr *preply_addr;
preply_addr = Malloc(servlen);
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
len = servlen;
n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {
printf("reply from %s (ignored)\n",
Sock_ntop(preply_addr, len));
continue;
}
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
可以看下这段代码有什么问题, 当对端机器只有一个网卡是没问题的,当服务器有多个网卡就会判断错误,遇到这种情况怎么办呢,对端机器要针对每块网卡进行bind操作,用多路转接进行监听
如果服务器没有启动,客户端udp连接发送sendto,可能成功,但是实际上发送失败,因为异步错误不会返回给程序
udp客户发送的临时端口不能改变,但是ip可以改变
UDP默认缓冲区大小为42080字节
tcp和udp可以绑定同意端口
名字与地址转换
gethostbyname把主机名字映射为IPV4地址,gethostbyaddr执行相反的映射
getservbyname是根据服务名称,协议名称,获取ip地址信息
getservbyport类似
下图是getaddrinfo中hints中ai_family 和 ai_socktype的对应关系
gai_strerror可以打印出 getaddrinfo错误信息
同样getaddrinfo既可以处理ipv4,也可以处理ipv6
UDP套接字不需要设置SO_REUSEADDR选项
getnameinfo是getaddrinfo的互补函数
gethostbyname,gethostbyaddr,getservbyname,getservbyport都是不可重入的
inet_pton,inet_atop都是可重入的
高级套接字编程
当服务器既有ipv4网卡和ipv6网卡时,如果客户端是ipv4,那么与服务器通信时会映射为ipv6地址,但是实际上两者的通信方式依然是ipv4通信
当ipv6客户端指向一个ipv4映射的ipv6地址时,那么实际上通信方式依然是ipv4
守护进程和inetd超级服务器
int
daemon_init(const char *pname, int facility)
{
int i;
pid_t pid;
if ( (pid = Fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* parent terminates */
/* child 1 continues... */
if (setsid() < 0) /* become session leader */
return (-1);
Signal(SIGHUP, SIG_IGN);
if ( (pid = Fork()) < 0)
return (-1);
else if (pid)
_exit(0); /* child 1 terminates */
/* child 2 continues... */
daemon_proc = 1; /* for err_XXX() functions */
chdir("/"); /* change working directory */
/* close off file descriptors */
for (i = 0; i < MAXFD; i++)
close(i);
/* redirect stdin, stdout, and stderr to /dev/null */
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
openlog(pname, LOG_PID, facility);
return (0); /* success */
}
需要充分理解此函数
高级IO函数
connect超时时间一般为75秒
可以使用alarm使的超时时间减短,但是最大超时时间就是75秒
也可以使用SO_RVTTIMEO进行设置
Unit域协议
unit域和普通的区别是只能在本机通信
bind时会bind一个本地地址
socketpair仅仅适用于unix套接字,比较类似pipe函数
在一个未绑定的Unix套接字上发送数据报不会自动给这个套接字捆绑一个路径名
socketpair也可以来传递文件描述符,通过带外数据进行读取
ssize_t
read_fd(int fd, void *ptr, size_t nbytes, int *recvfd)
{
struct msghdr msg;
struct iovec iov[1];
ssize_t n;
#ifdef HAVE_MSGHDR_MSG_CONTROL
union {
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
#else
int newfd;
msg.msg_accrights = (caddr_t) &newfd;
msg.msg_accrightslen = sizeof(int);
#endif
msg.msg_name = NULL;
msg.msg_namelen = 0;
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
if ( (n = recvmsg(fd, &msg, 0)) <= 0)
return(n);
#ifdef HAVE_MSGHDR_MSG_CONTROL
if ( (cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
if (cmptr->cmsg_level != SOL_SOCKET)
err_quit("control level != SOL_SOCKET");
if (cmptr->cmsg_type != SCM_RIGHTS)
err_quit("control type != SCM_RIGHTS");
*recvfd = *((int *) CMSG_DATA(cmptr));
} else
*recvfd = -1; /* descriptor was not passed */
#else
/* *INDENT-OFF* */
if (msg.msg_accrightslen == sizeof(int))
*recvfd = newfd;
else
*recvfd = -1; /* descriptor was not passed */
/* *INDENT-ON* */
#endif
return(n);
}
贴一个关键代码,这块代码主要实现了进程间的fd传递
非阻塞IO
对于一个非阻塞的io,当缓冲空间不够时,会发送一个EWOULDBLOCK的错误,
非阻塞模式下,connect很可能会返回EINPROGRESS错误码
#include "unp.h"
int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
int flags, n, error;
socklen_t len;
fd_set rset, wset;
struct timeval tval;
flags = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
error = 0;
if ( (n = connect(sockfd, saptr, salen)) < 0)
if (errno != EINPROGRESS)
return(-1);
/* Do whatever we want while the connect is taking place. */
if (n == 0)
goto done; /* connect completed immediately */
FD_ZERO(&rset);
FD_SET(sockfd, &rset);
wset = rset;
tval.tv_sec = nsec;
tval.tv_usec = 0;
if ( (n = Select(sockfd+1, &rset, &wset, NULL,
nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return(-1);
}
if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
return(-1); /* Solaris pending error */
} else
err_quit("select error: sockfd not set");
done:
Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
if (error) {
close(sockfd); /* just in case */
errno = error;
return(-1);
}
return(0);
}
这段代码首先设置fd非阻塞,如果connect的结果是EINPROGRESS,则代码三次握手还未完成,通过select设置一个超时时间,等到超时时间结束后
服务器accept建议使用非阻塞模式,因为当监听到fd可读时,如果在accept之前客户端就断开,那么accept就会阻塞,所以让accept为非阻塞是很有必要的
ioctl操作
ioctl通常会进行以下6类操作
套接字操作,文件操作,接口操作,ARP高速缓存操作,路由表操作,流系统
路由套接字
广播
ipv4支持广播,ipv6不支持
通常广播地址是ip最后是255的地址
内核不允许对广播数据报执行分片
多播
ipv4的D类地址,224.0.0.0 到 239.255.255.255,是ipv4多播地址,
IP_ADD_MEMBERSHIP 和 MCAST_JOIN_GROUP 类似
高级UDP套接字编程
如果程序想要支持多播和广播,那必须要使用UDP
UDP没有连接建立和删除,只需要两个分组就可以交换一个请求和一个应答,tcp却需要20个分组
tcp有一些udp的优势,如果想要一些特性,必须由应用程序自行提供他们,包括以下
如果想要请求重传式程序使用UDP,那么必须在客户程序中增加以下两个特性,1) 超时和重传 2)序列号
如果创建一个并发UDP服务器,1 当与客户只发送几个消息,可以fork一个子进程,并让子进程处理
2 与客户端交换多个消息,可以让服务端为每个客户创建一个套接字,在其上bind一个临时端口,然后使用该端口发送对该客户的所有应答
带外数据
带外数据也叫做加速数据,带外数据被认为比普通数据有更高的优先级
如果新的OOB在旧的OOB被读取之前就到达,那么旧的OOB数据会被丢弃
当紧急数据到达实际的缓冲区,该数据字节可能被拉出带外,也有可能被留在带内,SO_OOBILLINE默认是禁止的
如果进程多次读入同一个带外字节,读入操作将返回EINVAL
内核检测到oob数据发送过来时,需要注册SIGURG, 并且设置文件描述符为F_SETOWN
如果设置了在线数据,读位置总是从带外数据开始的
带外数据可以时任何8位值
SO_KEEPALIVE 在两个小时没有数据交换时,会自动发送一个探针
信号驱动式IO
信号驱动IO时内核有数据时,通过应该注册的回调函数进行通知,信号名称是SIGIO
IP选项
ipv4允许在20个字节首部固定部分之后跟以最多共40个字节的选项
这些选项可以通过IP_OPTIONS套接字选项
可以通过getsockopt或setsockopt进行设置