基本TCP套接口编程
4.1 过程
4.2 socket
#include <sys/socket.h>
int socket(int family, int type, int protocal);
返回值:成功返回非负整数,与文件描述符类似,称为套接口描述字,简称套接字(sockfd)。
注意调用完socket函数之后只是有了一个某一协议下的套接字,并没有指明地址之类的。
- family:协议族
family | 说明 |
---|---|
AF_INET | IPv4 |
AF_INET6 | IPv6 |
AF_LOCAL | unix协议域 |
AF_xxx(address family??)地址族。
- type:套接口类型
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接口 |
SOCK_DGRAM | 数据报套接口 |
SOCK_RAW | 原始套接口 |
- protocol:协议
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP |
IPPROTO_UDP | UDP |
IPPROTO_SCTP | SCTP |
4.3 connect
从4.1中的图中可以看出connect是客户端调用的一个函数。
客户端在调用connect之前不用调用bind函数,内核会选择IP地址与端口。
connect函数的形式是:
#include <sys/socket.h>
int connect(int sockfd, const sockaddr* servaddr, socklen_t addrlen);
//返回值:0-成功,1-出错
参数解析:
- sockfd:调用socket()函数返回的套接字;
- servaddr:指向服务器的套接口地址结构体的指针;
- addrlen:结构体的长度;
如果是TCP套接口,客户端在调用connect函数时,会触发三次握手。
而且connect函数只有在成功或出错时才会返回。
也就是说,如果connect函数返回了,表明已经成功建立连接或者出错了。
如果连接失败会怎么样呢?会出现什么类型的连接失败呢?
-
客户端没有收到服务器对的SYN的响应
如果超时,客户端会重发SYN,但是多次重发之后无响应则返回ETIMEDOUT错误。 -
服务器对客户端发出的SYN的响应为RST
服务器没有在客户端指定的端口上监听的进程。返回ECONNREFUSED. -
客户端发出的SYN在某个路由器出现目的不可达
如果connect成功了,那么将从SYN_SENT状态转变为established。
如果connect失败了,将直接进入CLOSED状态,则下次不能再使用本次的套接字,必须关闭。
4.4 bind
将IP地址与端口赋予一个套接口。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
//返回值:0-成功, -1-出错
比较关键的是第二个参数,这里有IP地址与端口号。
bind可以绑定IP地址与端口号,也可以不绑定。
对于客户端,如果不绑定IP地址,则内核在connect的时候会自动选择;
对于服务器,内核把客户端发送的SYN中的目的IP地址当做服务器的源IP地址。
如果指定的端口号为0,则内核在bind时会选择一个端口;
如果指定的IP地址为通配地址,那么内核在已连接(TCP)或者发送数据报(UDP)时才选择IP地址。
关于通配地址:IPv4的通配地址为INADDR_ANY,一般为0。
4.5 listen
只用于服务端。
形式:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
作用:
1)使用socket函数创建套接字时,默认创建的是主动套接字,也就是说他会主动发起connect,但是对于服务器端,应该是被动地被连接,所以listen函数就把主动套接字变为被动套接字,使得服务端从CLOSED状态转变为LISTEN状态。
2)规定内核应该为相应套接口排队的最大连接数。
这里说的最大连接数是什么意思呢?
实际上,内核为监听套接口维护两个队列,分别是:
- 未完成连接队列:SYN已经由客户端发出并到达服务器,但是服务器正在等待进行TCP三次握手的过程,此时套接口处于SYN_RCVD状态。
- 已完成连接队列:每个已完成TCP三次握手的客户对应一项。套接口处于established状态。
从上图可以看出,每到达一个SYN,TCP在未完成队列中创建一个新项,三次握手完成之后,该项就从未完成队列转移到已完成连接队列的尾部,该项将一直存在直到三次握手完成或者超时。当进程调用accept时,从已完成队列中取出队头。
4.6 accept
服务器调用,用处:从已完成连接队列队头返回下一个已完成连接。如果队列为空,则进入睡眠(假设套接口为阻塞方式)。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
参数struct sockaddr* cliaddr
用来返回客户端地址信息。
这个函数会返回一个套接字,注意返回的套接字为已连接套接字,和第一个参数“监听套接字”不同。
一个服务器通常只有一个监听套接字,但是每建立一个连接就会产生一个新的已连接套接字。监听套接字的生命周期与服务器进程生命周期相同,但是已连接套接字在连接释放之后就被关闭了。
4.8 fork与并发服务器
为每一个新到来的连接创建一个进程,在父进程中继续监听,在子进程中完成对客户需求的处理。
连接建立之后,accept返回一个已连接套接字connfd,此时调用fork,在父进程中直接关闭connfd,在子进程中关闭listenfd,执行操作然后退出。
框架:
pid_t pid;
int listenfd, connfd;
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
for(;;) {
connfd = accept(listenfd, ...);
if( (pid = fork()) == 0 ) { // child proc
close(listenfd); //子进程关闭listefd
do_something(); //子进程处理函数
close(connfd); //关闭connfd
exit(0); //子进程退出
}
// father proc
close(connfd);//父进程直接关闭connfd, 然后等待下一个连接
}
调用close会导致发送一个FIN,但是上面的例子中父进程close(connfd)
但是没有断开与客户端的连接,这是为什么呢?
其实是因为执行fork函数时,套接字connfd的引用计数加一了,在父进程中执行close并没有将connfd的引用计数变为0,所以连接没断。
注意:fork之后的文件描述符以及套接字由父子进程共享,因此引用计数会加一。
子进程关闭listenfd
,父进程关闭connfd
的过程:
4.9 close
#include <unistd.h>
int close(int sockfd);
默认行为:将套接口标记为已关闭,立即返回,被标记为已关闭的套接口不能再被write与read函数调用。
但是这种默认行为可以通过某种方式改变,如套接口选项SO_LINGER。
在上一节并发服务器中说了文件描述符与引用计数的问题,父进程中关闭只会使得相应的套接字引用计数减一,只有在引用计数变为0之后才会触发FIN的发送。
如果就是想发送一个FIN,则应该调用shutdown
函数。
父进程一定记得关闭connfd,否则会耗尽套接字,而且连接不会被终止。
getsockname()返回本地地址;
getpeername()返回客户端地址;