tcp提供了可靠传输,当tcp向另一端发送数据的时候,要求对端返回一个确认。如果没有接收到确认,tcp就重传数据并且等待更长时间,数次重传失败后,tcp才放弃。
建立一个tcp连接会发生如下事情:
- 服务器必须准备好接受外来的连接请求,通常通过socket,bind,listen这三个函数完成。
- 客户端通过connect连接服务器,主动打开,导致tcp客户端发送一个SYN字节,通常SYN不携带数据。
- 服务端必须确认客户的SYN字节,即ACK字节,同时服务端还会发送一个SYN字节给客户端
- 客户端必须确认服务端的SYN。
每一个SYN可以包含多个TCP选项:
- MSS选项:向对端通告最大分节大小,即愿意接收的最大数据量。
- 窗口规模选项:TCP首部中接收窗口首部为16位,意味着最大接收窗口为65535.
- 时间戳选项:对于高速网络是必要的,可以防止失而复现的分组可能造成的数据损坏。
相关函数:
(1)socket()
#include <sys/socket.h> int socket(int family, int type, int protocol);
- family:指定协议族,也称为协议域。取值为下面中的一个
- type,指明套接字类型
- protocol:协议类型常值,设为0的话表示选择所给定的family和type组合的系统默认值。
(2)connect()
#include <sys/socket.h> int connect(int sockfd,const struct sockaddr* servaddr, socklen_t addrlen);
成功返回0,否则返回-1
- sockfd:是由socket返回的套接字。
- servaddr:包含服务器IP地址和端口号的套接字地址结构
- addrlen:套接字地址结构的大小。
客户在调用connect之前不必一定需要调用bind函数,内核会确定源IP并选择一个临时端口作为源端口。如果是TCP套接字,调用connect将激发TCP三次握手过程。函数会阻塞进程,直到成功返回或者出错。
- 返回ETIMEDOUT错误:内核发送一个SYN,如没有相应,则等待6s后在发送一个,若仍无相应,则等待24秒再发送,总共等待75s后仍没有响应,则返回该错误。
- 返回ECONNREFUSED:服务器对客户端响应一个RST,表明服务器在客户指定的端口上没有进程在等待与之连接。产生RST还有两个条件:1)TCP想取消一个已有连接;2)TCP接收到一个根本不存在的连接上的分节
- 返回EHOSTUNREACH或ENETUNREACH错误:客户发出的SYN在中间的某个路由引发一个“目的地 不可达”ICMP错误,内核会报错该消息,并按情况1中的间隔继续发送SYN,若在规定时间内仍未收到响应,则把报错的信息作为这两种错误之一返回给进程。
connect若失败,则该套接字不可用,则必须先关闭该套接字,然后再次调用connect函数。
(3)bind函数:把一个本地协议地址赋予一个套接字
#include<sys/socket.h> int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
调用bind可以绑定IP地址或者端口,可以两者都指定,也可以两者都不指定。
- 如果指定端口为0,那么内核在bind被调用得时候选择一个临时端口。
- 如果指定IP地址为通配地址(对IPv4来说,通配地址由常值INADDR_ANY来指定,值一般为0),内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址
如果让内核来为套接字选择一个临时端口号,函数bind并不会返回所选择的值。第二个参数有const限定词,它无法返回所选的值。如果想得到内核所选择的临时端口值,必须调用getsockname函数
(4)listen函数
#include<sys/socket.h> int listen(int sockfd, int backlog);
listen做两件事:
- 当socket创建一个套接字的时候,它被设定为一个主动套接字。listen将其转成一个被动套接字,指示内核应接受指向该套接字的连接请求
- 第二个函数规定了内核应该为相应套接字排队的最大连接个数。
内核为任一给定的监听套接字维护两个队列,两个队列之和不超过backlog
- 未完成队列
- 已完成队列
当进程调用accept函数的时候,如果已连接队列不为空,那么队头项将返回给进程。否则进程进入睡眠,直到TCP在该队列中放入一项才唤醒它;不要把backlog设置为0;设置backlog的一种方法是使用一个常值。SYN到达的时候,如果队列已满,TCP忽略该SYN分节。
(5)accept()
#include<sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t addrlen);
- sockfd:监听套接字描述符
- cliaddr:已连接的对端客户的套接字地址结构
- addrlen:调用时指示内核向cliaddr写入的最大字节数。
如果对返回客户协议地址不感兴趣,可以把cliaddr和addrlen均置为空指针。
(6)close()
#include<unistd.h> int close(int sockfd);
close一个TCP套接字的默认行为是套接字标记关闭,立即返回调用进程。然后TCP将尝试发送已排队等待发送到对端的任何数据。close会将套接字描述符的引用计数减1,如果引用计数仍大于0,则不会引起TCP的四次挥手终止序列。
(7)shutdown()
#include<sys/socket.h> int shutdown(int sockfd, int howto);
- howto
- SHUT_RD:关闭连接的读这一半,套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数(对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃)
- SHUT_WR:关闭连接的写这一半(对于TCP,称为半关闭),套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。进程不能再对这样的套接字调用任何写函数
- SHUT_RDWR:连接的读半部和写半部都关闭。等价于调用2次shutdown,分别指定SHUT_RD与SHUT_WR
shutdown和close的区别:
- 关闭套接字的时机不同:close把描述符的引用计数减一,仅在引用计数为0的时候,才会关闭套接字,而shutdown可直接关闭套接字
- 全关闭和半关闭:close终止读和写两个方向的数据传送。shutdown可以关闭一个也可以关闭两个。
(8)getsockname()和getpeername()
这两个函数与TCP连接建立过程中套接字地址结构的信息获取相关
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t *adrlen); int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t *adrlen);
- getsockname用于返回与某个套接字关联的本地协议地址
- 没调用bind的TCP客户上,connect成功返回后,获取由内核赋予该连接的本地IP地址和端口号
- 在以端口号0调用bind后,返回由内核赋予的本地端口号。
- 获取某个套接字的地址簇
- 绑定通配IP的服务器上,accept返回后,获取由内核赋予该连接的本地IP地址,此时sockfd不是监听套接字描述符
- getpeername用于返回与某个套接字关联的外地协议地址
- 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能获取客户身份的唯一途径便是调用getpeername
(9)fork()
#include <unistd.h> pid_t fork(void);
- fork():调用进程(父进程)中返回一次,返回值是新建的子进程的进程ID号,在子进程又返回一次。返回值为0.返回值本身告知当前进程是父进程还是子进程。