第四章笔记
1. 基本Tcp客户端/服务器程序的套接字函数
2. socket函数:
int socket(int family,int type,int protocol);
(1)socket有三个函数,除了tcp udp外还支持许多协议。
(2)对于tcp协议:三个参数分别为AF_INET/AF_INET6、SOCK_STREAM、0
(3)对于udp协议:三个参数分别为AF_INET/AF_INET6、SOCK_DGRAM、0
(4)其他协议和参数含义,先pass
3. connect函数:
int connect(int sockfd,connect struct sockaddr* servaddr,socklen_t addrlen);
(1)正如第三章所示:第二个参数是通用的套接字地址结构指针,但是传入参数时需要指定具体的套接字地址结构
(2)客户在调用connect前不必非得调用bind函数,因为内核对确定源IP地址,并选择一个临时端口作为源端口
(3)connect激发Tcp三次握手过程。
(4)connect可能出错的情况:
a: 若客户机与服务器断连,Tcp客户端没有收到SYN分节的响应,返回ETIMEDOUT错误。举例:对于4.4BSD,当内核发送SYN,若无响应6s后再发送,若仍无响应再24s后再发送,若总共等了75s仍未响应返回ETIMEDOUT
经过测试,运行time intro/daytimetcpcli 10.0.0.1,经过2m7s左右才返回connect time out的错误
b: 若服务器主机在我们指定的端口上没有进程在等待与之连接,则对客户的SYN的响应是RST。这是一种硬错误(hard error),当客户一接收到RST就马上返回ECONNREFUSED错误。
c: 当客户发出的SYN在中间的某个路由器引发了一个目的地不可达的ICMP错误,这认为是一种软错误,之后按第一种情况所属的时间间隔继续发SYN。若在某个规定时间仍未收到响应,则返回EHOSTUNREACH或ENETUNREACH错误。
(4)当connect成功返回,当前套接字进入ESTABLISHED状态,即三次握手成功状态。若失败则该套接字不再使用,如果需要再次调用connect时,必须close当前的套接字都重新调用socket
4. bind函数:
int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);
(1)绑定的IP地址和端口号,可以指定通配IP地址和端口号0。如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。
(2)对于IPv4,通配地址由常量INADDR_ANY(0)来指定。但是IPv6的IP地址是128位,不是简单类型,不能像IPv4那样用简单数值常量表示。
IP地址用通配地址赋值代码:
struct sockaddr_in saddr;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY定义在头文件<netinet/in.h>
struct sockaddr_in6 saddr6;
saddr6.sin6_addr = in6addr_any;//系统预先分配in6addr_any变量并将其初始化常量IN6ADDR_ANY_INIT。头文件<netinet/in.h>含有in6addr_any的extern声明。
(3)传入bind的套接字地址IP和端口号不要忘记转换成网络字节序
(4)如果bind的是一个临时端口号,由于bind并不返回所选择的值,那么我们无法知道究竟bind了哪个端口号,可以调用函数getsockname来返回协议地址。
(5)bind返回的常见错误EADDRINUSE(地址已使用)
当绑定内置端口号(1-1024),必须有root权限,否则bind返回Permission denied错误。
5. listen:
int listen(int sockfd,int backlog)
(1)backlog的含义:内核为监听套接字维护的已完成连接队列(以完成三次握手,状态为ESTABLISHED,并正等待accpet)总个数的最大值
(2)backlog到底设置多少是合理的呢????
(3)当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,而不是立即响应RST。因为这种情况是暂时的,客户端会等一段时间会重发SYN,期望不久能在这些队列中找到可用空间。
6. accept:
int accept (int sockfd,struct sockaddr* cliaddr,socklen_t* addrlen);
(1)作用:用于从已完成连接队列返回下一个已完成连接。如果已完成连接队列为空,那么进程被进入睡眠(如果套接字为默认的阻塞方式)。
(2)如果对返回的套接字地址不感兴趣,cliaddr和addrlen可以设置为NULL
(3)accept返回的cliaddr,IP地址和端口号都是网络字节序,如果要打印出来查看,需要先转换成主机字节序,之后端口号可以直接打印,IP地址之后需要再次调用inet_ntop来获取字符串格式
6. close:
int close(int sockfd);
(1)作用:将套接字句柄引用计数减1,如果引用计数降为0,则将套接字标记为关闭,并立即返回到调用进程。而Tcp协议栈则尝试发送已排序等待发送到对端的任何数据,之后发送FIN分节,接收端(协议栈)收到后传递给应用程序一个文件结束符。之后接受端发送ACK和FIN。之后发送端再次发送ACK。
(2)标记为关闭的套接字之后不能再被进程调用,既不能再用于read和write
7. shutdown:
shutdown没有句柄引用计数的概念,它的作用是立即向对端发送FIN,并且shutdown之后的套接字只不能用于写,但可用于读
8. getsockname getpeername
int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t* addrlen);
int getpeername(int sockfd,struct sockaddr* peeraddr,socklen_t* addrlen);
(1)getsockname:返回与某个套接字关联的本地协议地址(IP地址和端口号和地址族)
(2)getpeername:返回与某个套接字关联的对端协议地址(IP地址和端口号和地址族)
(3)需要这两个函数的理由:
a. 在没有调用bind或以端口号0调用bind的客户程序中,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和端口号。
b. 用通配IP地址调用bind的服务器程序中,getsockname用于返回内核赋予该连接的本地IP地址。注意:传入的套接字描述符必须是已连接套接字描述符,而不是监听套接字描述符
c. 如果不知道传入的套接字地址具体是哪个地址族,可以传入sockaddr_storage,该结构能承载系统支持的任何套接字地址结构的空间大小。
int sockfd_to_family(int sockfd)
{
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if(getsockname(sockfd,(sockaddr*)&ss,&len) < 0;
return -1;
return (ss.ss_family);
}
9. 并发服务器
(1)fork返回值>0的,表示当前进程是父进程,fork返回的是子进程的pid。fork返回值=0的,表示当前进程是子进程,子进程可调用getppid获取父进程的pid。
(2)当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被整个客户端长期占用,而是希望同时服务多个客户。Unix中编写并发服务器最简单的办法就是fork一个子进程来服务每个客户。
(3) 实现过程:当通过accept获取一个客户请求,然后fork一个子进程,子进程首先关闭监听监听套接字,并处理客户请求。父进程则关闭连接套接字,并再次调用accept获取下一个客户请求。
(4)使用fork对关闭套接字的处理:父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。父进程调用accept之后调用fork,所接受的监听套接字和已连接套接字则由父子进程共享。通常,子进程关闭监听套接字,接着读写这个已连接套接字;父进程不要忘记关闭这个已连接套接字。原因见下面注释。
(5)fork的子进程处理完连接套接字描述符后,不要忘记调用exit退出进程,否则会执行到父进程的代码。
(5)典型的并发服务器程序轮廓:
pid_t pid;
int listenfd,connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd,LISTENQ);
for(;;)
{
connfd = Accept(listenfd, ... );
if( (pid = Fork()) == 0)
{
Close(listenfd); //因为exit会终止进程,而进程终止处理的部分工作就是关闭所有由内核打开的描述符,所有close listenfd可写可不写
doit(connfd);
Close(connfd);
exit(0); //不要忘记调用exit,来关闭该进程
}
Close(connfd); //不要忘记close connfd,因为父进程不会用到connfd, close connfd不一定会真的关闭进程,它只是把进程的引用计数减一。如果父进程不关闭connfd,即使子进程close connfd也不会真正关闭connfd,导致连接一直打开着。而且这将导致父进程耗尽可用描述符,因为任何进程在任何时刻可拥有的打开着的描述符通常是有限的。
}
10. 总结:
(1)传入bind的套接字地址IP和端口号不要忘记转换成网络字节序
(2)bind指定的IP地址可以为通配IP地址,表示内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。对于IPv4用INADDR_ANY指定,对于IPv6用in6addr_any指定。bind指定的端口号可以为0,表示内核自己分配端口
(3)若connect失败后则该套接字不再使用,如果需要再次调用connect时,必须close当前的套接字都重新调用socket
(4)accept返回的套接字地址是网络字节序,如果对accept返回的套接字地址不感兴趣,cliaddr和addrlen可以设置为NULL;如果需要读取accept返回的套接字地址,需要先从网络字节序转换从主机字节序。
(5)close只是将套接字句柄引用计数减1,如果引用计数降为0,才将套接字标记为关闭,之后进程不能再对close的套接字进行任何调用
(6)对套接字标记为关闭后,Tcp协议栈则尝试发送已排序等待发送到对端的任何数据,之后发送FIN分节,接收端(协议栈)收到FIN后传递给应用程序一个文件结束符通知应用程序收到了对端的FIN,之后两端发送的网络终止序列不在累述
(7)getsockname和getpeername用于返回与某个套接字关联的本地和对端协议地址(IP地址 端口号 地址族)
(8)使用fork实现并发服务器:子进程不要忘记关闭监听套接字,接着读写这个已连接套接字;父进程不要关闭这个已连接套接字;fork的子进程处理完连接套接字描述符后,不要忘记调用exit退出进程,否则会执行到父进程的代码。典型的并发服务器程序轮廓见9.(5)