创建一个可用的套接字:
int socket(int domain, int type, int protocol)
- domain PF_INET、PF_INET6以及PF_LOCAL等,即套接字类型
-
type
- SOCK_STREAM 字节流,对应TCP;
- SOCK_DGRAM 数据报,对应UDP;
- SOCK_RAW 原始套接字。
- protocol 原本用来指定通信协议,但现在基本废弃。因为通过前面两个参数已经指定协议。所以protocol一般写成0即可。
创建的socket若需被使用,就得调bind函数,把socket和socket地址绑定。
bind(int fd, sockaddr * addr, socklen_t len)
- sockaddr * addr 通用地址格式,传入的参数可能是IPv4、IPv6或本地socket格式。
- len 传入的地址长度,bind函数会根据该字段判断传入的参数addr怎么解析。
可以把bind函数理解成这样:
bind(int fd, void * addr, socklen_t len)
不过BSD设计socket大约1982年,C还没void *语法,为解决该问题,BSD设计者们创造性设计了通用地址格式,作为支持bind和accept等这些函数的参数。
对于使用者,每次需将IPv4、IPv6或本地socket格式转化为通用socket格式,就像下面的IPv4 socket地址格式:
struct sockaddr_in name; bind (sock, (struct sockaddr *) &name, sizeof (name)
对实现者,可根据该地址结构的前两个字节判断出是哪种地址。为处理可变长结构,需要读取函数里的len参数,即可解析和判断地址。
设置bind时,对地址和端口可以有多种处理方式。 可将地址设置成本机IP地址,告诉os内核,仅处理目标IP是本机IP地址的IP包。
但写代码时不知道将会被部署到啥机器,通配地址解决该问题,告诉os内核只要目标地址是咱们的都行。比如一台机器有两块网卡,IP地址分别是202.61.22.55和192.168.1.11,那么向这两个IP请求的请求包都会被我们的程序处理。
2 配置通配地址- IPv4,使用INADDR_ANY
- IPv6,使用IN6ADDR_ANY
struct sockaddr_in name; /* IPV4通配地址 */ name.sin_addr.s_addr = htonl (INADDR_ANY);3 端口
如果把端口设置成0,就相当于把端口的选择权交给内核,内核会根据算法选择一个空闲端口,完成套接字绑定。这在服务器端不常使用。
一般服务器端的程序一定要绑定到一个知名端口。服务器端的IP地址和端口数据,相当于打电话拨号时需要知道的对方号码,如果没有电话号码,就没有办法和对方建立连接。
初始化IPv4 TCP 套接字的案例:
#include #include #include #include int make_socket (uint16_t port) { int sock; struct sockaddr_in name; /* 创建字节流类型的IPV4 socket. */ sock = socket (PF_INET, SOCK_STREAM, 0); if (sock < 0) { perror ("socket"); exit (EXIT_FAILURE); } /* 绑定到port和ip. */ name.sin_family = AF_INET; /* IPV4 */ name.sin_port = htons (port); /* 指定端口 */ name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */ /* 把IPV4地址转换成通用地址格式,同时传递长度 */ if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0) { perror ("bind"); exit (EXIT_FAILURE); } return sock }
listen:接上电话线,一切准备就绪。
bind函数只是让socket和地址关联。如果要让别人打通电话,还需要我们把电话设备接入电话线,让服务器真正处于可接听状态,这就需要listen函数。
初始化创建的socket,之后会主动发起请求(调用connect函数)。通过listen函数,可将原来的"主动"socket转换为"被动"socket,告诉os内核:我这个socket是用来等待用户请求。os内核会为此做好接收用户请求的一切准备,如完成连接队列。
listen函数原型int listen (int socketfd, int backlog)
- socketfd socket描述符
- backlog 未完成连接队列的大小,即可以接收的并发数目。越大,并发数理论上越大。但参数过大也会占用过多系统资源,所以Linux并不允许对这个参数进行改变。
当客户端的连接请求到达时,服务器端应答成功,连接建立,这时内核需把该事件通知到应用程序,让应用程序感知到这个连接。
accept这个函数的作用就是连接建立后,内核和应用程序之间的桥梁。
4.1 原型int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
- listensockfd 套接字,通过bind,listen一系列操作而得到的套接字。返回值有两部分,cliadd是通过指针方式获取的客户端的地址,addrlen地址的大小 函数的返回值,代表与客户端的连接。
两个socket描述字:
- 输入参数,监听socket描述字listensockfd
- 返回的已连接socket描述字
网络程序需并发处理,不可能一个应用程序运行后只能服务一个客户。所以监听socket一直都存在,服务成千上万的客户,直到这个监听socket关闭。
一旦一个客户和服务器连接成功,完成TCP三次握手,os内核就为该客户生成一个已连接套接字,让应用服务器使用该已连接套接字和客户进行通信。若应用服务器完成对该客户的服务,如一次网购下单,一次付款成功,那么关闭的就是已连接套接字,就完成TCP连接释放。
这时释放的只是这一个客户连接,其它被服务的客户连接可能还存在。最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务。
5 客户端发起连接的过程第一步:建立一个套接字,不一样的是,客户端需调用connect发起请求。
5.1 connect客户端和服务器端的连接建立,是通过connect函数完成的。
connect的构建函数int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
- sockfd 连接套接字,通过前面讲述的socket函数创建
- servaddr、addrlen 指向套接字地址结构的指针和该结构的大小。 套接字地址结构必须含有服务器的IP地址和端口号。
客户在调用函数connect前,不必非得调用bind函数,若需要,内核会确定源IP地址,并选择一个临时端口作为源端口。
若是TCP套接字,则调用connect函数将激发TCP三次握手,而且仅在连接建立成功或出错时才返回。其中出错返回可能有如下 case:
- 三次握手无法建立,客户端发出的SYN包没有任何响应,于是返回TIMEOUT错误。这种情况比较常见的原因是对应的服务端IP写错
- 客户端收到RST(复位)回答,这时候客户端会立即返回CONNECTION REFUSED错误。这种情况比较常见于客户端发送连接请求时的请求端口写错,因为RST是TCP在发生错误时发送的一种TCP分节
产生RST的三个条件:
- 目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器(如前所述)
- TCP想取消一个已有连接
- TCP接收到一个根本不存在的连接上的分节
客户发出的SYN包在网络上引起"destination unreachable",即目的不可达错误。这种情况比较常见的原因是客户端和服务器端路由不通。 根据不同的返回值,我们可以做进一步的排查。
这里的网络编程模型都是阻塞式,即调用发起后不会直接返回,由os内核处理后,才会返回。 相对的,还有一种叫做非阻塞式。
6 TCP三次握手服务器端通过socket,bind和listen完成被动套接字的准备工作,被动,即等别人来连接,然后调用accept,就会阻塞在这里,等待客户端的连接来临。
客户端调用socket和connect后,也会阻塞。接下来由os内核网络协议栈在工作。
- 客户端的协议栈向服务器端发SYN包,并告诉服务器端当前发送序列号j,客户端进入SYNC_SENT状态
- 服务器端的协议栈收到该包后,和客户端进行ACK应答,应答值为j+1,表示对SYN包j的确认,同时服务器也发个SYN包,告诉客户端当前我的发送序列号为k,服务器端进入SYNC_RCVD状态
- 客户端协议栈收到ACK后,使应用程序从connect调用返回,表示客户端到服务器端的单向连接建立成功,客户端状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为k+1
- 应答包到达服务器端后,服务器端协议栈使得accept阻塞调用返回。这时服务器端到客户端的单向连接也建立成功,服务器端也进入ESTABLISHED状态
TCP三次握手建立连接的过程中,内核通常会为每一个LISTEN状态的Socket维护两个队列:
- SYN队列(半连接队列):这些连接已经接到客户端SYN
- ACCEPT队列(全连接队列):这些连接已经接到客户端的ACK,完成了三次握手,等待被accept系统调用取走
比如 Tomcat 的Acceptor负责从ACCEPT队列中取出连接,当Acceptor处理不过来时,连接就堆积在ACCEPT队列中,这个队列长度也可以通过参数设置。
7 总结- 服务器端通过创建socket,bind,listen完成初始化,通过accept完成连接的建立
- 客户端通过创建socket,connect发起连接建立请求