4.1概述
这一章节就是关于基本TCP套接字编程了。
编写一个基本的TCP客户服务端程序的套接字函数调用流程一般:
首先学习网络编程所需要的基本套接字函数。
4.2 socket()
若要执行网络IO, 一个进程要做的第一件事就是创建套接字,调用socket函数。
#include <sys/socket.h>
int socket(int family, int type, int protocol);
family参数指定协议族;type参数指定套接字类型;protocol参数通常设置为0。
出错返回-1,正常返回一个非负的整数,作为套接字描述符sockfd。
family | 说明 |
AF_INET | IPv4协议族 |
AF_INET6 | IPv6协议族 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
type | 说明 |
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据包套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
protocol | 说明 |
IPPROTO_CP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
4.3 connect()
TCP客户端用connect函数与服务器建立连接。
#include <sys/socket.h>
int connect(int sockfd, const strcut sockaddr *servaddr, socklen_t addrlen);
参数sockfd是socket()的返回值;第二个第三个参数分别是指向套接字地址结构的指针和该结构的大小。
成功返回0,出错返回-1。
客户端在调用connect函数前,不用非得调用bind函数去绑定端口;因为内核会确定源IP,给一个临时的端口作为源端口。
connect函数成功返回,则会处于ESTABLISHED状态;若connect失败,则必须先关闭套接字,再尝试调用connect。
4.4 bind()
bind()函数把一个本地协议set给套接字。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
第二个第三个参数分别是指向该协议的地址结构的指针和该结构的长度。
bind()可以绑定一个端口,也可以绑定一个IP地址,也可以两者都绑定,也可以什么都不绑。
前面我们提到,在调用connect函数或listen函数之前,不必非得调用bind(),因为内核会为相应的套接字选择一个临时端口。bind并不会返回所选择的值,你需要用函数getsocketname来返回。
但该规则对于远程调用RPC(Remote Procedure Call)是例外的。内核会为他们的监听套接字 选择一个临时端口,该端口通过RPC端口映射器注册,客户在connect服务器之前,必须与端口映射器联系以获取他们的临时端口。
进程可以把一个特定的IP绑定到它的套接字上, 这就决定了TCP客户在该套接字上发送的IP数据报的源地址,TCP服务器只能接受目的地为该IP地址的客户连接。这种场景通常用在主机为多个组织提供web服务器上。
一般TCP客户是不会绑定IP到套接字上的。内核会根据用到的外出接口作为源IP,即客户发送的SYN的目的IP作为源IP,外出接口则取决于达到服务器的路径。
IP地址 | 端口 | 结果 |
通配地址 | 0 | 内核选择IP和端口 |
通配地址 | 非0 | 内核选择IP,进程指定端口 |
本地IP | 0 | 进程指定IP,内核选择端口 |
本地IP | 非0 | 进程指定IP和端口 |
内核选择IP,对于IPv4来说,通配地址由常值INADDR_ANY指定,其值一般是0,因为IPv4的IP地址是一个32位的值。
struct sockaddr_in servaddr;
servaddr.sin_adddr.s_addr = htonl(INADDR_ANY)
但IPv6的IP地址是放在结构里面,不能直接赋值
struct sockaddr_in6 serv;
serv.sin6_addr = INN6ADDR_ANY_INET;
4.5 listen()
listen函数仅被TCP服务器调用。
#include <sys/socket.h>
int listen(int scokfd, itn backlog);
它做两件事:
socket创建完套接字后,listen函数监听这个套接字,表示内核应该接受指向该套接字的连接请求
第二个参数规定了内核为该套接字维护最大连接个数;
成功返回0,出错返回-1。
listen的backlog参数维护的是两个的队列总和的最大值(未完成连接的队列和已完成连接的队列)
未完成连接的队列中的任何一项的存留时间就是一个RTT。
backlog的值不能定义为0。历史中总是喜欢把backlog的值设置为5,你也可以设置一个很大的值甚至超服务器能处理的值(如果超过,服务器会自动截断为自己能接受的最大值)
当客户发送一个SYN想建立连接的时候,如果队列满了,那服务器会自动忽略掉,让TCP的重传机制来处理,而不是立即响应一个RST。
4.6 accept()
服务器调用accept()函数,从已完成连接的队列的队头返回下一个连接。如果队列为空,则进程睡眠
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
成功返回非负值,出错返回-1。
第一个参数是监听套接字,就是由socket创建,随后用作bind和listen的第一个参数的套接字。
一个服务器只会创建一个监听套接字,生命周期内会一直存在,在完成对某个给定客户的服务后,相应的连接套接字就会被关闭。
accept返回的是一个由内核新生成的,代表与客户的已连接套接字。
4.7 fork() 和 exec()
#incldue <unistd.h>
pid fork(void);
fork用于创建一个新进程。调用fork一次,它会返回两次。一次是对调用进程返回派生进程的id,一次是对派生进程返回0,。
任何子进程只有唯一一个父进程,可以用getppid获取父进程的id。
父进程会有多个子进程,没有函数用于获取子进程的pid,只有在调用fork的时候记录下子进程的pid。
fork的使用场景:1)创建进程副本;2)一个进程想执行另一个程序:进程调用fork创建进程副本,副本进程调用exec将程序替换为想执行的程序。
exec()用来执行程序。其实有6个这样的函数,不过不管哪个函数被调用,我们都统称为exec函数
这六个函数的区别在于:1)待执行的程序是由文件名file指定还是路径path指定;2)参数是一个一个列出来arg并以一个空指针作为结束还是放在参数数组argv中的;3)是把调用进程的环境变量传递还是为新程序指定一个新的环境变量。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ..., /* (cahr*)0 */);
int execv(const char *pathname, char *const *argv[]);
int execlp(cosnt char *filename, const char *arg0, ..., /* (cahr*)0 */);
int execle(const char *pathname, const char *arg0, ..., /* (cahr*)0*, char *const envp[] */);
int execvp(const char *filename, char *const argv[]);
int execve(const char *pathname, char *const argv[], char *const envp[]);
需要注意的是:1)argv数组中必须有一个指向其末尾的空指针;envp数组一样;2)使用当前的PATH环境变量将filename转换为pathname,但如果filename存在一个/,就不再用PATH环境变量了。此外pathname是全限定名;3)
4.8 并发服务器
前文的UNIX网络编程学习记录2-第一章 1.5、获取时间的服务端程序 用来处理简单的时间获取程序是足够的。但如果服务器处理一个客户端程序需要很久的时间,总不能让他一个人占着服务器吧?我们希望一个服务器可以服务多个客户端程序,简单的做法就是fork出一堆子进程。
pid_t pid;
int listenfd, connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);
for (; ; ) {
connfd = Accept(listenfd, ...);
if ((pid == Fork()) == 0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
建立连接后,当accept返回后,进程就会fork出一些子进程,由子进程服务客户,父进程就会关闭已连接套接字。
每个文件或套接字都有一个引用计数,引用计数为0 才会做资源释放。
如果非得想在某个TCP连接上发送一个SYN,可以使用shutdown()函数代替。
4.9 close()
#include <unistd.h>
int close(int sockfd);
关闭后的套接字不能再被使用。
4.10 getsockname() 和 getpeername()
分别返回套接字有关的本地协议地址和外地协议地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *loacladdr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
使用场景:
1)在没有调用bind绑定具体的地址和端口号的时候,connect成功后,需要用getsockname返回该连接的本地地址和端口号;
2)调用bind,但参数是0(内核选择端口),需要用getsockname返回该连接本地端口。
3)getsockname可以返回该套接字的地址族;
4)使用bind绑定通配IP地址,accept成功返回的时候,getsockname的参数是已连接套接字而不是监听套接字。
下面是一个返回某个套接字地址族的简单处理:
#include "unp.h"
int sockfd_to_family(int sockfd) {
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if (getsockname(sockfd, (SA*)&ss, &len) < 0) {
return -1;
}
return ss.ss_family;
}
大多数TCP服务器是并发的,大多数UDP服务器是迭代的。