socket函数
socke函数的功能在于生成一个指定类型的套接字描述符
头文件 <sys/socket.h>
int socket(int family, int type, int protocol);
其中family指协议族,family可选值如下:
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
type可选值如下:
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAM | 原始套接字 |
protocol一般设置为0,以选择给定family喝type组合的系统默认值
family指定为AF_INET或AF_INET6时,
当SOCKET_STREAM指定为SOCK_STREAM,表示TCP套接字
当SOCKET_STREAM指定为SOCK_DGRAM,表示UDP套接字
当成功时,socket函数的返回一个非负整数,表示套接字描述符,当失败时返回-1
bind函数
头文件 <sys/socket.h>
bind函数把一个套接字描述符和一个协议地址相绑定,绑定成功返回0,失败返回-1
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
第一个参数是套接字描述符,第二个参数是指向套接字地址结构的指针,第三个参数是该结构的大小
端口指定:调用connect或者listen时,相应的套接字都应该有一个绑定的端口。如果一个客户或者服务器没有调用bind函数把sockfd捆绑到一个端口,内核就会自动为相应套接字提供一个临时端口。客户使用临时端口是正常的,然而服务器使用临时端口却是罕见的,因为这样客户在连接的时候,便无法知道自己应该连接到哪个端口
IP地址指定:对于TCP服务器,套接字捆绑一个ip地址,表示限定该服务器只接收目的地址为该ip的客户链接,一般不指定
如果没有指定,则服务器就把客户发送的SYN的目的地址设为服务器的源IP地址
对于客户,表示把套接字捆绑到客户端的IP地址,即把该IP地址作为源IP地址,通常不指定,这样连接套接字时,内核根据所用外网接口选择源IP地址
bind函数返回的常见错误是EADDRINUSE,表示地址已使用
总结:客户端一般不指定ip地址,也不指定端口,因此不用bind函数
服务端一般不指定ip地址,但指定端口,因此会把sockaddr_in中的ip地址指定为0,端口指定为具体端口
IPv4服务器IP设定代码通常如下:
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htol(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//SERV_PORT在头文件unp.h,指定为9877
IPv6的情况下,ip地址用一个数组进行存储,没办法直接赋值,因此指定方法如下:
servaddr.sin6_addr = in6addr_any;
connect函数
头文件 <sys/socket.h>
int connect(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
调用connect将激发三次握手,且仅在连接建立成功或者失败才返回,成功返回0,失败返回-1
其中失败可能因为以下几种错误:
- 客户发一个SYN,若无响应等6s再发一个,若无响应等24s再发一个,若总共等75s后仍然为收到响应,则返回ETIMEDOUT
- 若服务器主机在指定的端口没有进程在等待与之连接(这是一种硬错误),则对客户返回RST(表示复位),客户收到RST马上返回ECONNREFUSED
- 若客户发出的SYN在中间某个路由器引发了一个目的地不可达的ICMP错误(这是一种软错误),则按1的时间间隔进行发送,若在75s后仍然未收到响应,则把EHOSTUNREACH或者ENETUNREACH返回
RST是一种TCP分节,产生的三个条件包括:
- 目的地为某端口的SYN到达,但是该端口没有监听的服务器
- TCP想取消一个已有连接
- TCP接受到一个根本不存在的连接的分节
listen函数
头文件 <sys/socket.h>
int listen(int sockfd, int backlog)
使用socket函数创建一个套接字时,创建是主动套接字,客户用该套接字发起连接
listen把主动套接字转化为被动套接字,即监听套接字
内核中为每一个给定的监听套接字维护两个队列:
- 未完成连接队列:服务器收到客户的SYN报文后,把该客户添加到未完成连接队列队尾,并返回SYN/ACK报文
- 已完成连接队列:服务器收到客户的ACK报文后,将改客户从未完成连接队列移动到已完成连接队列的队尾
sockfd是由socket函数创建的套接字,backlogs 规定为两个队列的总和最大值的上限,经常设为5
backlogs不能设为0,如果不想让任何客户连接到监听套接字,就关掉该监听套接字
当一个客户SYN到达时,如果两个队列都是满的,TCP就忽略该分节,也不返回RST。因为等下次客户再重发过来时,该分节就很可能能进队列了
accept函数
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
其中sockfd为监听描述符
accept函数由TCP服务器调用,用于从已连接队列队头返回一个已完成连接,如果队列为空,则进程进入睡眠
如果accept成功,返回值是一个由内核生成的全新描述符,代表所返回客户的TCP连接
连接客户的具体信息存在cliaddr和addrlen中,如果不想获取客户具体信息,可以把第二、三个参数都设为NULL
三路握手完成后,在服务器调用accept之前返回的数据会存在响应已连接套接字的接受缓冲区
fork和exec函数
头文件 <unistd.h>
pid_t fork();
fork函数会基于当前进程创建一个副本,从而创建一个新的进程,原来的进程称为父进程,新的进程称为字进程
fork函数会在父进程中返回字进程的pid, 而在子进程返回0
在子进程中可以调用getppid得到父进程的pid
pid_t getppid();
创建完子进程后,如果想要执行另一个程序,一般会调用exec函数把自身替换成新的程序
进程调用exec函数后,原来打开着的描述符会继续打开,除非使用fcntl把FD_CLOEXEC禁止掉
exec包括六个函数,具体如下:
int execl(const char *pathname, const char *arg, ...)
int execv(const char *pathname, char *const argv[])
int execle(const char *pathname, const char *arg, ..., char *const envp[])
int execve(const char *pathname, char *const argv[], char *const envp[])
int execlp(const char *filename, const char *arg, ...)
int execvp(const char *filename, char *const argv[])
如果执行成功,函数不会返回,控制将被传递到新程序的起点
如果出错:返回-1,失败原因记录在error中
查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径(pathname),而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
前4个取路径名做参数,后两个则取文件名做参数。
当指定filename做参数时:
a. 如果filename中包含/,则将其视为路径名
b. 否则就按PATH环境变量搜索可执行文件。
参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举(l)的方式,而另一种则是将所有参数整体构造成指针数组(v)进行传递。
环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码,PATH环境变量定义时目录之间需用用“:”分隔,以“.”号表示结束。PATH环境变量定义在用户的.profile或.bash_profile中,下面是PATH环境变量定义的样例,此PATH变量指定在“/bin”、“/usr/bin”和当前目录三个目录进行搜索执行码。
PATH=/bin:/usr/bin:.
exec函数族关系:
前4位 | 统一位:exec | |
第5位 | l: 参数传递为逐个列举方式 | |
v: 参数船体为构造指针数组的方式 | ||
第6位 | e:可传递新进程环境变量 | |
p: 可执行文件查找方式为文件名 |
exec函数调用举例:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
close函数
头文件 <unistd.h>
int close(int sockfd);
关闭成功返回0,出错返回-1
每个打开的描述符都会带有引用计数器,当调用socket或accept函数返回时,对应的套接字描述符引用数为1
然而调用fork函数返回后,描述符就会在子进程和父进程之间共享,导致描述符的引用计数为2
其中一个进程调用close函数,都只会使引用计数减1,只有引用计数为0时,才会发生套接字的资源释放
这时TCP会尝试发送已排队等待发送到对端的所有数据,发送完毕后发送FIN报文,开始四次握手
getsockname和getpeername函数
头文件 <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
getsockname获取sockfd套接字所对应的本地地址
getpeername获取sockfd套接字所对应的对方地址
需要这两个函数的原因是因为:
子进程调用exec函数替换进程映像后,原进程的变量都会丢失,从而造成不便