TCP
1、可靠传输
TCP 提供了可靠传输,当 TCP 向另一端发送数据时,要求对端返回一个确认。如果没有收到确认, TCP就重传数据并等待更长时间。在数次重传失败后, TCP 才放弃,如此在尝试发送数据上所花的总时间一般为 4~10 分钟(依赖于具体实现)
2、缓冲区
每一个 TCP 套接字有一个发送缓冲区,可以使用 SO_SNDBUF 套接字选项更改该缓冲区的大小。当某个进程调用 write 时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。
如果套接字的发送缓冲区容不下进程的所有数据,或者说发送缓冲区已经有其它数据了,该进程将进入休眠状态(套接字默认阻塞)内核将不从write返回,直到进程缓冲区的所有数据被复制到套接字发送缓冲区。
因此, 写一个 TCP 套接字的 write 调用成功返回仅仅表示我们可以重新用原来的应用进程缓冲区,并不表明对端的 TCP 或应用进程已接收到数据。
TCP套接字从发送缓冲区发送数据到对端,只有收到对端ACK的确认,才会丢弃发送缓冲区中被确认的数据。
3、相关函数
int socket(int socket,int type,int protocol)
//protocol:协议类型常值。设为 0 的话表示选择所给定 family 和 type 组合的系统默认值
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen)
调用 connect 前不必非得调用 bind,如果没有 bind,内核会确定源 IP 并选择一个临时端口作为源端口。如果是 TCP 套接字,调用 connect 将激发 TCP 三路握手过程,函数会阻塞进程,直到成功或出错才返回。
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen)
//如果指定端口号为 0,内核在 bind 被调用时选择一个临时端口
//如果指定 IP 地址为通配地址(对 IPv4 来说,通配地址由常值 INADDR_ANY 来指定,值一般为 0),
//内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地 IP 地址
如果让内核来为套接字选择一个临时端口号,函数 bind 并不返回所选择的值。第二个参数有 const 限定词,它无法返回所选的值。如果想得到内核所选择的临时端口值,必须调用 getsockname 函数
int listen(int sockfd,int backlog)
//当 socket 创建一个套接字时,套接字被假设为一个主动套接字, listen 将其转成一个被动套接字,
//指示内核应接受指向该套接字的连接请求
//第二个参数规定了内核应为相应套接字排队的最大连接个数
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen)
accept 用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,那么进程被投入睡眠
int close(int sockfd)
close把socket计数描述符的引用计数减1,仅在该计数变为0时关闭套接字
内核为任一给定的监听套接字维护两个队列,两者数量不超过backlog
1、未完成连接队列(SYN队列)
职责是回复SYN+ACK包,并且在没有收到ACK包时重传,直到超时;发送完SYN+ACK之后,
SYN队列等待从客户端发出的ACK包(也即三次握手的最后一个包)。当收到ACK包时,
首先找到对应的SYN队列,再在对应的SYN队列中检查相关的数据看是否匹配,如果匹配,
内核将该连接相关的数据从SYN队列中移除,创建一个完整的连接,并加入已连接队列。
2、已完成连接队列(accept队列)
Accept队列中存放的是已建立好的连接,也即等待被上层应用程序取走的连接。
当进程调用accept(),这个socket从队列中被取出,传递给上层应用程序操作。
过程发生在客户端的connect(),服务端listen()和accept()间。
UDP
1、缓冲区
UDP 是不可靠的,不必保存应用进程数据的一个副本,所以不需一个真正的发送缓冲区。UDP缓冲区仅表示能写到该套接字的 UDP 数据报的大小上限。从写一个 UDP 套接字的 write 调用成功返回,表示所写的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或它的某个片段,内核通常会返回一个 ENOBUFS 错误给它的应用进程。
UDP 层中隐含有排队发生。事实上每个 UDP 套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区。当进程调用 recvfrom 时,缓冲区中的下一个数据报以 FIFO 顺序返回给进程、这样,在进程能够读该套接字中任何已排好队的数据报之前,如果有多个数据报到达该套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中。
2、相关函数
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);
相比recv和send多了后面两个参数,用于指明发送者或接受者的套接字地址结构。
sendto()函数主要用于SOCK_DGRAM类型套接口向dest_addr参数指定端的套接口发送数据报。recvfrom()同理,src_addr是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。UDP是单进程,单处理方式,当多个进程与服务端通信,src_addr只能保存最后一个套接字地址用于之后的通信,也就是只能与最后一个发送的套接字进程通信。
当你对于数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
3、UDP与epoll结合
UDP svr无法充分利用epoll的高性能event机制的主要原因是,UDP svr只有一个UDP socket来接收和响应所有client的请求。然而如果能够为每个client都创建一个socket并虚拟一个“连接”与之对应,这样不就可以充分利用内核UDP层的socket查找结果和epoll的通知机制了么。server端具体过程如下:
第一次收到一个新的client的UDP数据包,就创建一个新的UDP socket和这个client对应,这样接下来的数据交互和事件通知都能准确投递到这个新的UDP socket fd了。