基本TCP套接字编程
在学习具体函数前,必须先有这样一个认识,socket和各种相关函数的实质是什么?socket源于Unix,而Unix有着“一切皆文件”的哲学思想,socket是“open-write/read-close”模式的实现,那么socket就会提供接口函数来实现对其进行相应操作。
1.socket():
在成功时返回一个小的非负整数值,它与文件描述符类似,称为套接字描述符。
2.connect():
在此之前,不必非得调用bind函数,因为需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或失败时才返回。
connect函数导致当前套接字从CLOSED状态(该套接字自从由socket函数创建以来一直所处状态)转移到SYN_SENT状态;若connect成功再转移到ESTABLISHED状态,若失败则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数。
3.bind():
把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
TCP服务器不同于TCP客户,需要捆绑它们的众所周知端口被大家认识。
4.listen():
仅由TCP服务器调用。当socket函数创建套接字时,它被假设为一个主动套接字,即一个将调用connect发起连接的客户套接字。而listen函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求。
调用listen导致套接字从CLOSED状态转换到LISTEN状态。
另外,必须认识到内核为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列:每个SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应TCP三次握手过程,这些套接字处于SYN-RCVD状态。
- 已完成连接队列:每个已完成TCP三次握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态。
5.accept():
由TCP服务器调用,用于从已完成连接队列返回下一个已完成连接,若队列为空则进程投入睡眠,直到TCP在该队列中投入一项才唤醒它。
那么accept正确返回后,接下来发生了什么?
如果accept成功,那么其返回值是一个内核自动生成的全新描述符,代表与所返回客户的TCP连接。
讨论accept函数时,作为参数的是监听套接字描述符listenfd(由socket创建,随后用作bind和listen参数的描述符),作为返回值的是已连接套接字描述符connfd:一个服务器仅创建一个监听套接字,它在服务器生命期内一直存在,而内核为服务器进程接受的每个客户连接创建一个已连接套接字,当服务器完成对某给定客户的服务时,相应已连接套接字就被关闭(结合下面论述理解:已连接套接字在每次循环中关闭,但监听套接字在整个有效期内保持开放)。
6.并发编程机制
Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户:
图注:
3中父进程关闭connfd,子进程关闭listenfd;
4中达到两个套接字所期望的最终状态,即子进程处理与客户连接,父进程可在监听套接字上再次调用accept来处理下一个客户连接,这从1中服务器和4中服务器父进程对照可看出。
结合上面处理流程就不难理解“对一个TCP套接字调用close会导致发送一个FIN,随后是正常的TCP连接终止序列,但为什么父进程对connfd调用close没有终止与客户的连接?”这个问题,因为在每个文件或套接字都有一个引用计数(在文件表项中维护),它是当前打开着的引用该文件或套接字的描述符个数。如:socket返回后与listenfd关联的文件表项的引用计数值为1,accept返回后与connfd关联的文件表项的引用计数值也为1;在fork返回后,两个描述符在父子进程间共享,因此引用计数值均为2。这么一来,当父进程关闭connfd时,它只是把引用计数值从2减为1,该套接字真正的清理和资源释放要等到引用计数值为0时才发生。
/*------------------------------------------------- * Name: head.h * Date: 2015/10/08 ------------------------------------------------*/ #ifndef _HEAD_H_ #define _HEAD_H_ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <netinet/in.h> #include <errno.h> #include <time.h> #include <assert.h> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> #include <dirent.h> #include <signal.h> #include <sys/time.h> #include <arpa/inet.h> void perror_exit(char *s) { perror(s); exit(EXIT_FAILURE); } #endif
/*------------------------------------------------- * Name: server.c * Date: 2015/10/08 ------------------------------------------------*/ #include "head.h" int main(int argc, char *argv[]) { int listenfd; listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(listenfd < 0) perror_exit("socket error"); else printf("create listenfd successfully...\n"); struct sockaddr_in serveraddr; memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(5188); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //inet_aton("127.0.0.1", $serveraddr.sin_addr); int on=1; if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))<0) perror_exit("setsockopt error"); if(bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) perror_exit("bind error"); else printf("bind address successfully...\n"); if(listen(listenfd, SOMAXCONN) < 0) perror_exit("listen error"); else printf("keep listening...\n"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int connfd; int count = 0; pid_t pid; while(1) { if((connfd = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) perror_exit("accept error"); else printf("client %d connect to server: ip = %s, port = %d\n", ++count, inet_ntoa(peeraddr.sin_addr), peeraddr.sin_port); pid = fork(); if(pid == -1) perror_exit("fork error"); else if(pid > 0) close(connfd); else { close(listenfd); while(1) { char buf[1024]; memset(buf, 0, sizeof(buf)); int ret = read(connfd, buf, sizeof(buf)); if(ret == -1) perror_exit("read error"); else if(ret == 0) { printf("client %d was closed...\n", count); break; } else { fputs(buf, stdout); int len = strlen(buf); buf[len-1] = '*'; buf[len] = '\n'; write(connfd, buf, ret); } } exit(EXIT_SUCCESS); } } return 0; }
/*------------------------------------------------- * Name: client.c * Date: 2015/10/08 ------------------------------------------------*/ #include "head.h" int main(int argc, char *argv[]) { int socketfd; socketfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(socketfd == -1) perror_exit("socket error"); else printf("create socketfd successfully...\n"); struct sockaddr_in clientaddr; memset(&clientaddr, 0, sizeof(clientaddr)); clientaddr.sin_family = AF_INET; clientaddr.sin_port = htons(5188); clientaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int conn; conn = connect(socketfd, (struct sockaddr *)&clientaddr, sizeof(clientaddr)); if(conn == -1) perror_exit("connect error"); else printf("connect to server successfully...\n"); char recvbuf[1024]; char sendbuf[1024]; memset(recvbuf, 0, sizeof(recvbuf)); memset(sendbuf, 0, sizeof(sendbuf)); int ret; pid_t pid; pid = fork(); if(pid == -1) perror_exit("fork error"); else if(pid > 0) { while(1) { ret = read(socketfd, recvbuf, sizeof(recvbuf)); if(ret == -1) perror_exit("read error"); else if(ret == 0) { printf("server was closed...\n"); break; } else fputs(recvbuf, stdout); } close(socketfd); } else { while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(socketfd, sendbuf, sizeof(sendbuf)); memset(sendbuf, 0, sizeof(sendbuf)); } exit(EXIT_SUCCESS); } return 0; }