基本TCP套接字编程
一、socket函数
1 #include <sys/soket.h> 2 int soket(int family, int type, int protocol); 3 //返回:若成功则为非负描述符,若出错则为-1
family参数指明协议族,type参数指明套接字类型, protocol设为协议类型常值或者设为为0。
AF_XXX和PF_XXX对比, AF_前缀表示地址族,PF_前缀表示协议族。
二、connect函数
TCP客户用connect函数来建立与TCP服务器的连接。
1 #include <sys/socket.h> 2 int connect(int sockfd, const struct sockeaddr *servaddr, socklen_t addrlen); 3 //返回:若成功则为0,若出错则为-1
sockfd是由socket函数返回的套接字描述符,第二第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,且仅在连接建立成功或出错时才返回。
出错返回的情况如下:
1)若TCP客户没有收到SYN分解的响应,则返回ETIMEDOUT错误。
2)若对客户的SYN的响应是RST(复位),则表明该服务器主机在我们指定的端口没有进程在等待与之连接(例如服务器进程没在运行)。
RST是TCP在发生错误时发送的一个分节(一种硬错误)。
产生RST的三个条件:目的地为某端口SYN到达,然而该端口上没有正在监听的服务器;
TCP想取消一个已有的连接;TCP接收到一个根本不存在的连接上的分节。
3)若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable" ICMP错误(一种软错误)。
三、bind函数
bind函数把一个本地协议地址赋予一个套接字。
1 #include <sys/socket.h> 2 int bing(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); 3 //返回:若成功则为0,若出错则为-1
第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是改地址结构的长度。
对于TCP,调用bid函数可以指定一个端口号,或指定一个IP地址,也可以是两者都指定,还可以都不指定。
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选则一个本地IP地址。
对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0。
无论是网络字节序还是主机字节序,INADDR_ANY的值(为0)都一样,因此使用htonl并非必须。
四、listen函数
listen函数仅由TCP服务器调用。
1)当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,他是一个将调用connect发起连接的客户套接字。
listen函数把一个未连接的套接字转换成一个被动套接字,只是内核应接受指向该套接字的连接请求。
2)
1 #include <sys/socket.h> 2 int listen(int sockfd, int backlog); 3 //返回:若成功返回0,若出错返回-1
第二个参数规定了内核应该为相应套接字排队的最大连接个数。
对于监听套接字的队列,如果三鹿握手正常完成,该项就从未完成连接队列中的队头项将返回给进程。
当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者弱国该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才能唤醒它。
- 不要把backlog定义为0),因为不同的实现对此有不同的解释。如果不向让任何客户连接到监听套接字上,那就关掉该套接字。
- 指定较大SYN分节的到达,未完成连接队列中的项数可能增长,他们等着三路握手的完成。
-
当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也即是不发送RST。另外,
-
客户无法区别响应SYN的RST九九意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了。
- 在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接受缓冲区大小。
五、accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程将被投入睡眠。
1 #include <sys/socket.h> 2 int accept(int sockfd, struct sockaddr *cialddr, socklen_t *addrlen); 3 //返回:若成功则为非负描述符,若尘出错则为-1
在讨论accept函数时,我们称它的第一个参数为监听套接字描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字描述符。
一个服务器通常仅仅创建一个套接字,它在该服务器的生命期内一直存在。
六、fork和exec函数
1 #include <unistd.h> 2 3 pid_t fork(void); 4 //返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
该函数是Unix中派生新进程的唯一方法。
fork的两个典型用法:
1)一个进程创建一个滋生的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理来自各自的某个操作。
这是网络服务器的典型用法。
2)一个进程想要执行另一个程序。该进程首先调用fork创建一个滋生的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。这是诸如shell之类程序的用法。
存放在硬盘上的可执行程序文件能够被Unix执行的唯一方法是:由一个现有的进程调用六个exec函数中的某一个。exec把当前进程映像替换
成新的程序文件,而且该新程序通常从main函数开始执行。进程ID并不改变。称调用exec的进程为调用进程(calling process),新执行的程序为
新程序(new program)。
七、并发服务器
Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。
以下为一个典型的并发服务器程序的轮廓。
1 pid_t pid; 2 int listenfd, connfd; 3 listenfd = Socket( ... ); 4 /*fill in sockaddr_in{} with server's well-know port*/ 5 Bind(listenfd, ...) 6 Listen(listenfd, LISTENQ); 7 for ( ; ; ) { 8 connfd = Accept(listenfd, ... ); /* probably blocks */ 9 if ( (pid = Fork()) == 0 ) { 10 Close(listenfd); /* child closes connected socket */ 11 doit(connfd); /* process the request */ 12 Close(connfd); /* child terminates */ 13 } 14 Closed(connfd); /* parent closes connected socket */ 15 }
每个文件或套接字都有一个引用计数。引用计数在文件表项中维护,它是当前打开着的引用该文件或套接字的描述符的个数。
套接字真正的清理和资源释放要等到其引用计数到达0时才发生。
八、close函数
通常的Unix close函数也用来关闭套接字并终止TCP连接。
1 #include <unistd.h> 2 int close(int sockfd); 3 //返回:若成功则为0,若出错则为-1
close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。
描述引用计数:并发服务器父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。
如果父进程对每个由accept返回的已连接套接字都不调用close,首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻拥有的打开着的描述符通常是有限制的,更重要的是,没有一个客户连接会终止。
九、getsockname和getpeername函数
这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地址协议地址(getpeername)。
1 #include <sys/socket.h> 2 int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen); 3 int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); 4 //返回:若成功则为0,若出错则为-1
两个函数最后一个参数都是值-结果参数。
- 在一个没有调用bind的TCP客户上 ,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。
- 在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号。
- getsockname可用于获取某个套接字的地址族。
- 在一个以通配IP地址调用bind的TCP服务器上,与某个客户端连接一旦建立,getsockname就可以用于返回由发i内核赋予该连接的本地IP地址。
- 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername。
小结
所有客户和服务器都从调用socket开始,它返回一个套接字描述符。客户随后调用connect,服务器则调用bind、listen和accept。套接字通常使用标准的close函数关闭。大多数TCP服务器是并发的,他们为每个待处理的客户连接调用fork派生一个子进程。大多数UDP服务器是迭代的。