下面我们用最简单的一对一的客户服务器模型来重现编程中遇到的一些问题:
初学socket的时候在编写socket程序的时候会遇到很多莫名其妙的问题,比如说bind函数返回的常见错误是EADDRINUSE
使用下面的程序重现这个状态:
client:
int main(int argc, const char * argv[])
{
struct sockaddr_in serverAdd;
bzero(&serverAdd, sizeof(serverAdd));
serverAdd.sin_family = AF_INET;
serverAdd.sin_addr.s_addr = inet_addr(SERV_ADDR);
serverAdd.sin_port = htons(SERV_PORT);
int connfd = socket(AF_INET, SOCK_STREAM, 0);
int connResult = connect(connfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
if (connResult < 0) {
printf("连接失败\n");
close(connfd);
return -1;
}
ssize_t writeLen;
ssize_t readLen;
char recvMsg[65535] = {0};
char sendMsg[20] = "I am client";
writeLen = write(connfd, sendMsg, sizeof(sendMsg));
if (writeLen < 0) {
printf("发送失败\n");
close(connfd);
return -1;
}
else
{
printf("发送成功\n");
}
while (1) {
// sleep(1);
readLen = read(connfd, recvMsg, sizeof(recvMsg));
if (readLen < 0) {
printf("读取失败\n");
close(connfd);
return -1;
}
if (readLen == 0) {
printf("服务器关闭\n");
close(connfd);
return -1;
}
printf("server said:%s\n",recvMsg);
}
close(connfd);
return 0;
}
server:
int main(int argc, const char * argv[])
{
struct sockaddr_in serverAdd;
struct sockaddr_in clientAdd;
bzero(&serverAdd, sizeof(serverAdd));
serverAdd.sin_family = AF_INET;
serverAdd.sin_addr.s_addr = htonl(INADDR_ANY);
serverAdd.sin_port = htons(SERV_PORT);
socklen_t clientAddrLen;
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
printf("创建socket失败\n");
close(listenfd);
return -1;
}
int bindResult = bind(listenfd, (struct sockaddr *)&serverAdd, sizeof(serverAdd));
if (bindResult < 0) {
close(listenfd);
printf("绑定端口失败,errno = %d\n",errno);
return -1;
}
else
{
printf("绑定端口成功\n");
}
listen(listenfd, 20);
int connfd;
unsigned char recvMsg[65535];
char replyMsg[20] = "I am server";
clientAddrLen = sizeof(clientAdd);
connfd = accept(listenfd,(struct sockaddr *)&clientAdd,&clientAddrLen);
if (connfd < 0) {
close(listenfd);
printf("连接失败\n");
return -1;
}
else
{
printf("连接成功\n");
}
ssize_t readLen = read(connfd, recvMsg, sizeof(recvMsg));
printf("readLen:%ld\n",readLen);
if (readLen < 0) {
printf("读取失败\n");
return -1;
}
else if (readLen == 0) {
printf("读取完成\n");
close(listenfd);
return 0;
}
printf("client said:%s\n",recvMsg);
while (1)
{
write(connfd, replyMsg, sizeof(replyMsg));
}
close(connfd);
return 0;
}
首先运行服务器程序,再运行客户端,然后关闭服务端后立马再打开服务端,就会打印如下信息:48对应 EADDRINUSE错误码
绑定端口失败,errno= 48
这里要说明一个问题:
当一个Unix进程无论自愿的(调用exit或者从main函数返回)还是非自愿的(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也将导致仍然打开的任何TCP连接上发出一个FIN。
很明显服务器已经关闭了,为什么会绑定端口失败呢,下面是TCP连接终止的四个分节:
某些情况下第一个分节的FIN随数据一起发送,另外,第二个和第三个分节有可能被合并成一个分节;
这里我们的服务器是主动关闭的一端,当主动发送FIN分节以后等待确认,状态变为FIN_WAIT_1,收到确认以后状态变为FIN_WAIT_2,收到客户端的FIN分节以后状态变为TIME_WAIT状态,这里在TIME_WAIT状态会停留2MSL后才会进入CLOSED状态;所以我们再立马启动服务器的时候,之前的连接还没有处于CLOSED状态,还存在者,所以就会绑定失败了;后面会讲到为什么会存在TIME_WAIT状态;
这里客户端是被动关闭的一端,收到服务端的FIN之后状态进入CLOSE_WAIT,这个时候read方法会返回0,然后发送对第一个分节的确认,此时客户端调用close方法发送FIN分节给服务端进入LAST_ACK状态,等待确认到达,收到确认以后连接状态变为CLOSED;
TIME_WAIT状态:停留在该状态的持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称为2MSL。MSL是任何IP数据报能够在因特网中存活的最长时间,最大值为255,这是一个跳数限制而不是真正的时间限制;
TIME_WAIT状态存在理由:
可靠地实现TCP全双工连接的终止:可能不得不重传最终那个ack,TIME_WAIT后是CLOSED,如果没有TIME_WAIT的2MSL,直接CLOSED,那么如果最后一个ACK丢失了,是不会重新再发送ACK的,那服务端收不到ACK就会重新发送最终那个FIN,这个时候客户端已经是CLOSED了,就会响应一个RST,这个RST就会被服务器解释成一个错误。
允许来的重复分节在网络中消逝:保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了,从而不会被误解成新连接的分组。
这里还有一种情况:打开客户端while里面的sleep,(或者屏蔽掉客户端下面的代码)然后再先运行服务器程序,再运行客户端,然后关闭服务端后立马再打开服务端,仍然会绑定失败,此时的状态和之前的有点不一样
if (readLen == 0) {
printf("服务器关闭\n");
close(connfd);
return -1;
}
我们从终端打印信息如下,此时服务器处于FIN_WAIT_2状态,就如上面说的因为客户端还没有关闭连接,没有发送第三个FIN分节,此时客户端因为已经收到来自服务端的FIN分节而处于CLOSE_WAIT状态;
wanglijuntekiMac-mini:~ wanglijun$ netstat -an |grep 8000
tcp4 0 0 192.168.1.103.8000 192.168.1.103.49632 FIN_WAIT_2
tcp4 290960 0 192.168.1.103.49632 192.168.1.103.8000 CLOSE_WAIT
这里有一个SO_REUSEADDR套接字选项,打开之后就能解决如上的问题,我们在band之前添加如下设置代码:
<span style="font-size:12px;">int yes = 1;
setsockopt(listenfd,
SOL_SOCKET, SO_REUSEADDR,
(void *)&yes, sizeof(yes));</span>
服务器重启监听时,试图捆绑现有连接上的端口会失败,(还有一种情况可能之前派生出来的子进程还处理着连接)如果设置了SO_REUSEADDR套接字选项,就会bind成功,所有的TCP服务器都应该指定SO_REUSEADDR套接字选项,以允许服务器在这种情形下被重新启动;SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
对于TCP,我们绝不可能启动捆绑相同IP地址和相同端口号的多个服务器。
参考:
《UNIX Network ProgrammingVolume 1, Third Edition: TheSockets Networking API》