第四章笔记
1. 基本Tcpclient/server程序的套接字函数
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)AF_LOCAL(或者是AF_UNIX)用于执行在相同机器的两个进程之间进行通信
(5)其它协议和參数含义,先pass
(6)返回值:假设失败返回-1,假设成功返回新创建的套接字描写叙述符。每一个进程在自己的进程空间里都有一个套接字描写叙述表。该表中存放着套接字描写叙述符和套接字数据结构地址的相应关系,而套接字数据结构都存放在操作系统的内核缓冲里。
3. connect函数:
int connect(int sockfd,connect struct sockaddr* servaddr,socklen_t addrlen);
(1)正如第三章所看到的:第二个參数是通用的套接字地址结构指针。可是传入參数时须要指定详细的套接字地址结构
(2)客户在调用connect前不必非得调用bind函数,由于内核对确定源IP地址。并选择一个暂时port作为源port
(3)connect激发Tcp三次握手过程。当客户收到三次握手的第二个分节时,connect就会返回,而server要直到收到三次握手的第三个分节才返回。
(4)connect可能出错的情况:
a: 若客户机与server断连,Tcpclient没有收到SYN分节的响应,返回ETIMEDOUT错误。举例:对于4.4BSD,当内核发送SYN,若无响应6s后再发送,若仍无响应再24s后再发送,若总共等了75s仍未响应返回ETIMEDOUT
经过測试,执行time intro/daytimetcpcli 10.0.0.1。经过2m7s左右才返回connect time out的错误
b: 若server主机在我们指定的port上没有进程在等待与之连接,则对客户的SYN的响应是RST。这是一种硬错误(hard error),当客户一接收到RST就马上返回ECONNREFUSED错误。
c: 当客户发出的SYN在中间的某个路由器引发了一个目的地不可达的ICMP错误,这觉得是一种软错误,之后按第一种情况所属的时间间隔继续发SYN。若在某个规定时间仍未收到响应,则返回EHOSTUNREACH或ENETUNREACH错误。
d: 由于信号导致的调用中断错误EINTR。注意:即使connect这个函数返回EINT,我们也不能再次调用它。否则会马上返回一个错误(5.9)。
所以出现EINTR能够採取和其它错误相同的处理方式,比方输出错误退出进程。
(4)当connect成功返回,当前套接字进入ESTABLISHED状态,即三次握手成功状态。若失败则该套接字不再使用,假设须要再次调用connect时,必须close当前的套接字都又一次调用socket
4. bind函数:
int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);
(0)对于第二个參数const struct sockaddr* myaddr,指向要绑定给sockfd的协议地址。
这个地址依据地址创建socket时的地址协议组的不同而不同,对于Ipv4要传递类型为sockaddr_in的地址,对于IPv6要传递类型为sockaddr_in6的地址,对于AF_UNIX要传递类型为sockaddr_un的地址。
(1)绑定的IP地址和port号。能够指定通配IP地址和port号0。
假设指定port号为0。那么内核就在bind被调用时选择一个暂时port。假设指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。
(2)对于IPv4,通配地址由常量INADDR_ANY(0.0.0.0)来指定。可是IPv6的IP地址是128位,不是简单类型,不能像IPv4那样用简单数值常量表示。
(3)bind通配地址是在告知系统,假设系统是多宿主机(多个IP),我们将接受目的地址为不论什么本地接口的连接
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和port号不要忘记转换成网络字节序
(4)假设bind的是一个暂时port号,由于bind并不返回所选择的值,那么我们无法知道究竟bind了哪个port号,能够调用函数getsockname来返回协议地址。
(5)bind返回的常见错误EADDRINUSE(地址已使用)
当绑定内置port号(1-1024),必须有root权限,否则bind返回Permission denied错误。
5. listen:
int listen(int sockfd,int backlog)
(1)backlog的含义:内核为监听套接字维护的已完毕连接队列(以完毕三次握手。状态为ESTABLISHED。并正等待accpet)总个数的最大值
(2)backlog究竟设置多少是合理的呢????
(3)当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,而不是马上响应RST。由于这样的情况是暂时的,client会等一段时间会重发SYN,期望不久能在这些队列中找到可用空间。
6. accept:
int accept (int sockfd,struct sockaddr* cliaddr,socklen_t* addrlen);
(1)作用:用于从已完毕连接队列返回下一个已完毕连接。假设已完毕连接队列为空,那么进程被进入睡眠(假设套接字为默认的堵塞方式)。
(2)假设对返回的套接字地址不感兴趣,cliaddr和addrlen能够设置为NULL
(3)accept返回的cliaddr。IP地址和port号都是网络字节序。假设要打印出来查看。须要先转换成主机字节序,之后port号能够直接打印,IP地址之后须要再次调用inet_ntop来获取字符串格式
(4)调用accpet时,假设出现EINTR或ECONNABORTED错误时能够忽略继续调用下一次accept。
EINTR错误是当信号发生时出现调用中断错误,而ECONNABORTED错误是当client调用connect与server建立三次握手协议后,client又发来RST,之后server调用accept就会返回ECONNABORTED。这是软错误,不须要退出程序,仅仅须要忽略它再次调用accept获取下一个可连接套接字。(5.11)
6. 读写函数:
网络IO操作有以下几组:
read()/write()
readv()/writev()
recv()/send()
redvmsg()/sendmsg()
recvfrom()/sendto()
最经常使用的是read/write。
对于read返回值大于0表示读取成功返回实际所读的字节数。0表示文件结束,对方调用了close;返回小于0的数表示出现了错误。假设错误是EINTR说明读是由中断引起的。write返回值大于0表示写成功。写了部分或者全部数据;返回值小于0表示出现了错误。
假设错误是EINTR表示写时出现了中断错误。
7. close:
int close(int sockfd);
(1)作用:将套接字句柄引用计数减1,假设引用计数降为0,则将套接字标记为关闭,并马上返回到调用进程。而Tcp协议栈则尝试发送已排序等待发送到对端的不论什么数据,之后发送FIN分节。接收端(协议栈)收到后传递给应用程序一个文件结束符。
之后接受端发送ACK和FIN。之后发送端再次发送ACK。
(2)标记为关闭的套接字之后不能再被进程调用,既不能再用于read和write
8. shutdown:
shutdown没有句柄引用计数的概念,它的作用是马上向对端发送FIN。并且shutdown之后的套接字仅仅不能用于写,但可用于读
9. 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地址和port号和地址族)
(2)getpeername:返回与某个套接字关联的对端协议地址(IP地址和port号和地址族)
(3)须要这两个函数的理由:
a. 在没有调用bind或以port号0调用bind的客户程序中。connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和port号。
b. 用通配IP地址调用bind的server程序中,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);
}
10. 简单的echo 服务端和client程序举例
服务端程序server.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //struct sockaddr_in
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG 50
#define MAXLINE 2048
#define handle_error(msg)
do { perror(msg); exit(EXIT_FAILURE); } while(0)
int main()
{
int listenfd,peerfd;
struct sockaddr_in sockaddr;
char buffer[MAXLINE] = {0};
if ((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
handle_error("create socket error");
}
memset(&sockaddr,0,sizeof(struct sockaddr_in));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockaddr.sin_port = htons(7777);
if(bind(listenfd,(struct sockaddr*)&sockaddr,sizeof(struct sockaddr_in)) == -1)
{
handle_error("bind");
}
if(listen(listenfd,LISTEN_BACKLOG) == -1)
{
handle_error("listen error");
}
while(1)
{
int readn;
struct sockaddr_in peeraddr;
socklen_t addrlen = sizeof(peeraddr);
char ip[20]={0};
memset(&peeraddr,0,sizeof(peeraddr));
if((peerfd = accept(listenfd,(struct sockaddr*)&peeraddr,&addrlen)) == -1)
{
handle_error("accept");
}
inet_ntop(AF_INET,&peeraddr.sin_addr,ip,20);
printf("peeraddr ip:%s port:%d
",ip,ntohs(peeraddr.sin_port));
readn = read(peerfd,buffer,MAXLINE);
if(readn > 0)
{
buffer[readn] = ' ';
printf("write:%s",buffer);
write(peerfd,buffer,readn);
close(peerfd);
}
else if(readn == 0)
{
close(peerfd);
}
else
{
close(peerfd);
perror("read");
}
}
close(listenfd);
return 0;
}
client程序client.cpp,client使用./client ip执行,每发送一次数据就结束程序
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define MAXLINE 2048
#define handle_error(msg)
do { perror(msg); exit(EXIT_FAILURE); } while(0)
int main(int argc,char* argv[])
{
int fd;
struct sockaddr_in serveraddr;
char buffer[MAXLINE] = {0};
if(argc != 2)
{
printf("usage:client ip
");
}
fd = socket(AF_INET,SOCK_STREAM,0);
if (fd == -1)
{
handle_error("create socket error");
}
memset(&serveraddr,0,sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
if(inet_pton(AF_INET,argv[1],&serveraddr.sin_addr) <= 0)
{
handle_error("inet_pton");
}
serveraddr.sin_port = htons(7777);
if(connect(fd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in)) == -1)
{
handle_error("connect error");
}
int n = read(0,buffer,1024); //读控制台:调fgets或 read(0,)
if( write(fd,buffer,n) < 0)
{
handle_error("write");
}
close(fd);
return 0;
}
11. 并发server
(1)fork返回值>0的,表示当前进程是父进程,fork返回的是子进程的pid。
fork返回值=0的。表示当前进程是子进程。子进程可调用getppid获取父进程的pid。
(2)当服端 务一个客户请求可能花费较长时间时,我们并不希望整个server被整个client长期占用。而是希望同一时候服务多个客户。
Unix中编写并发server最简单的办法就是fork一个子进程来服务每一个客户。
(3) 实现过程:当通过accept获取一个客户请求,然后fork一个子进程,子进程首先关闭监听监听套接字。并处理客户请求。父进程则关闭连接套接字,并再次调用accept获取下一个客户请求。
(4)使用fork对关闭套接字的处理:父进程中调用fork之前打开的全部描写叙述符在fork返回之后由子进程分享。父进程调用accept之后调用fork。所接受的监听套接字和已连接套接字则由父子进程共享。
通常,子进程关闭监听套接字,接着读写这个已连接套接字;父进程不要忘记关闭这个已连接套接字。原因见以下凝视。
(5)fork的子进程处理完连接套接字描写叙述符后,不要忘记调用exit退出进程。否则会执行到父进程的代码。
(5)典型的并发server程序轮廓:
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,导致连接一直打开着。
并且这将导致父进程耗尽可用描写叙述符。由于不论什么进程在不论什么时刻可拥有的打开着的描写叙述符一般是有限的。
}
12. 总结:
(1)传入bind的套接字地址IP和port号不要忘记转换成网络字节序
(2)bind指定的IP地址能够为通配IP地址,表示内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。对于IPv4用INADDR_ANY指定。对于IPv6用in6addr_any指定。bind指定的port号能够为0,表示内核自己分配port
(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地址 port号 地址族)
(8)使用fork实现并发server:子进程不要忘记关闭监听套接字。接着读写这个已连接套接字。父进程不要关闭这个已连接套接字;fork的子进程处理完连接套接字描写叙述符后,不要忘记调用exit退出进程,否则会执行到父进程的代码。
典型的并发server程序轮廓见11.(5)