网络通讯
数据传输三要素:
源、目的、长度
服务器:
被动的相应请求。
客户端:
主动的发起请求。
UDP和TCP:
UDP传输,如传输视频,某一帧数据丢失关系也不大,是无连接的传输;但是TCP主要是传输文件或者控制命令这些每一帧都很重要的信息,是可靠的有连接的传输。
TCP
文件读写:
fd=open(“文件名”);
read(fd,buf,len);
write(fd,buf,len);
服务器:
socket();
为通信创建一个节点并返回一个描述符。
bind()
把fd和ip端口绑定建立联系。
listen()
启动监测数据。
accept();
接受一条连接。等待connect()。
send发送数据,recv接收数据。
客户端:
socket()
客户端同样需要建立一个通信节点。
connect()
申请和服务器端建立连接。
建立连接后,send发送数据,recv接收数据。
具体连接过程可以参考TCP三次握手过程。
Linux系统是通过提供套接字(socket)来进行网络编程的.网络程序通过socket和其它几个函数的调用,
会返回一个 通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处.
我们可以通过向描述符读写操作实现网络之间的数据交流.
(一) socket
int socket(int domain, int type,int protocol)
domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等).
- AF_UNIX只能够用于单一的Unix 系统进程间通信,
- AF_INET是针对Internet的,因而可以允许在远程主机之间通信(当我们 man socket时发现 domain可选项是 PF_而不是AF_,因为glibc是posix的实现所以用PF代替了AF,不过我们都可以使用的)。
type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等)
- SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流.
- SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.
protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了
socket为网络通讯做基本的准备.成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况.
(二) bind
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket调用返回的文件描述符.
addrlen:是sockaddr结构的长度.
my_addr:是一个指向sockaddr的指针. 在中有 sockaddr的定义
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
我们主要使用Internet,所以
sin_family一般为AF_INET,
sin_addr设置为INADDR_ANY表示可以和任何的主机通信,(设置的是其成员sin_addr.s_addr)
sin_port是我们要监听的端口号.
sin_zero[8]是用来填充的.
需要注意的是:sin_addr、sin_port都需要经过字节序转换,将主机的字节序转化成网络字节序。接口函数为htons。
bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样
(三) listen
int listen(int sockfd,int backlog)
sockfd:是bind后的文件描述符.
backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度.
listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.
(四) accept
int accept(int sockfd, struct sockaddr *addr,int *addrlen)
-
sockfd:是listen后的文件描述符.
-
addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了.
accept成功时,返回最后的客户端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1
bind,listen和accept是服务器端用的函数.accept调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接.
(五) connect
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
-
sockfd:socket返回的文件描述符.
-
serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址
-
addrlen:serv_addr的长度
connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.
(六) recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数分别代表句柄,用于储存数据的buf,数据的长度len,flags用于特殊设置,在此设为0。返回值是接收数据的长度。
(七) send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数类型与recv一致。
程序设计:
目前最常用的服务器模型.
<一>循环服务器:循环服务器在同一个时刻只可以响应一个客户端的请求
<二>并发服务器:并发服务器在同一个时刻可以响应多个客户端的请求
TCP通讯为了实现并发服务器,常用的方法有两种:1.TCP服务器(建立子进程);2.多路复用IO
TCP服务器(建立子进程)
为了弥补循环TCP服务器的缺陷,人们又想出了并发服务器的模型. 并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是服务器创建一个 子进程来处理.
算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
if(fork(..)==0)
{
while(1)
{
read(...);
process(...);
write(...);
}
close(...);
exit(...);
}
close(...);
}
TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况. 不过也同时带来了一个不小的问题.为了响应客户机的请求,服务器要创建子进程来处理. 而创建子进程是一种非常消耗资源的操作.
下面代码实现:
首先实现socket的基本配置:
服务器端:
iSeverFd=socket(AF_INET,SOCK_STREAM,0);
if(iSeverFd<0)
{
printf("socket error!");
return -1;
}
tiSockaddrSeverIn.sin_family=AF_INET;
tiSockaddrSeverIn.sin_port=htons(SERVERIPPORT);/*自定义一个端口号*/
tiSockaddrSeverIn.sin_addr.s_addr=INADDR_ANY;
memset(tiSockaddrSeverIn.sin_zero,0,8);
iRet=bind(iSeverFd,(struct sockaddr *)&tiSockaddrSeverIn,sizeof(tiSockaddrSeverIn));
if(iRet<0)
{
printf("bind error!");
return -1;
}
iRet=listen(iSeverFd,999);
if(iRet<0)
{
printf("listen error!");
return -1;
}
尤其要注意的是这句:tiSockaddrSeverIn.sin_addr.s_addr=INADDR_ANY;
。写入参数的对象是结构体sin_addr的子成员s_addr。查了一下,这个结构体如下:
struct in_addr {
__be32 s_addr;
};
struct in_addr sin_addr;
INADDR_ANY是一个正整数,直接赋值给sin_addr一定会出错。
客户端中:
iClientFd=socket(AF_INET,SOCK_STREAM,0);
if(iClientFd<0)
{
printf("Client error!");
return -1;
}
tiSockaddrSeverIn.sin_family= AF_INET;
tiSockaddrSeverIn.sin_port= htons(SERVERIPPORT);/* host to net, short */
if (0==inet_aton(argv[1], &tiSockaddrSeverIn.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tiSockaddrSeverIn.sin_zero, 0, 8);
客户端的配置只需要一个scoket()函数。
随后客户端调用connect()函数。主动向客户端请求建立连接。
iRet=connect(iClientFd,(struct sockaddr *)&tiSockaddrSeverIn,sizeof(tiSockaddrClientIn));/*循环申请建立连接*/
if(iRet<0)
{
printf("connect error\n");
return -1;
}
此时,客户端进程正休眠在accpet()函数位置:
iClientFd=accept(iSeverFd,(struct sockaddr *)&tiSockaddrClientIn,&iRevNum);/*循环等待建立连接*/
如果有服务器有建立连接的请求,相应请求并返回一个句柄,用于随后与客户端通信;为实现与多个客户端建立连接,可以每把accept()从休眠中唤醒一次,就建立一个进程。
实现多个客户端连接一个服务端:
while(1)
{
iClientFd=accept(iSeverFd,(struct sockaddr *)&tiSockaddrClientIn,&iRevNum);/*循环等待建立连接*/
if(iClientFd<0)
{
printf("accept error!");
return -1;
}
else
{
if(!fork())/*创建一个进程用以接收数据*/
{
iClientNum++;
printf("Get connect from client %d : %s\n",iClientNum,inet_ntoa(tiSockaddrClientIn.sin_addr));
while(1)/*等待接收数据*/
{
iRevNumRel=recv(iClientFd,pcRevBuf,iRevNum,0);
if(iRevNumRel<=0)
{
printf("recv error");
close(iClientFd);
}
else
{
pcRevBuf[iRevNumRel]='\0';
printf("Get connect from client %d : %s\n",iClientNum,pcRevBuf);
/*收到数据之后在回发一次数据*/
send(iClientFd,pcRevBuf,iRevNum,0);
}
}
}
}
}
服务端发送数据的代码:
while(1)
{
if (fgets(pcSendBuf,iSendNum,stdin))
{
iSendNumRel=send(iClientFd,pcSendBuf,iSendNum,0);
if(iSendNumRel<=0)
{
printf("send error :%s\n",pcSendBuf);
}
else
{
iRevNumRel=recv(iClientFd,pcRevBuf,iSendNum,0);
printf("send :%s\n",pcRevBuf);
}
}
}
最后是通信的关闭,TCP通信的关闭也是具有特色的:由于相比于UDP通信其具有双向性,因此为了保证数据传输的完整,通常会选择只关闭一个方向(具体看四次挥手过程),此时可以调用int shutdown(int sockfd,int howto)
实现。
针对不同的howto,系统回采取不同的关闭方式。
- howto=0这个时候系统会关闭读通道.但是可以继续往接字描述符写.
- howto=1关闭写通道,和上面相反,着时候就只可以读了.
- howto=2关闭读写通道,和close一样
在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用shutdown,那么所有的子进程都不能够操作了,这个时候我们只能够使用close来关闭子进程的套接字描述符。
多路复用IO
为了解决创建子进程带来的系统资源消耗,人们又想出了多路复用I/O模型.
首先介绍一个函数select
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *except fds,struct timeval *timeout)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)
一般的来说当我们在向文件读写时,进程有可能出现阻塞,直到一定的条件满足. 比如我们从一个套接字读数据时,可能缓冲区里面没有数据可读 (通信的对方还没有 发送数据过来),这个时候我们的读调用就会等待(阻塞)直到有数据可读.如果我们不希望阻塞,我们的一个选择是用select系统调用. 只要我们设置好select的各个参数,那么当文件可以读写的时候select会"通知"我们 说可以读写了. 如果还没有满足条件,那么这个进程将休眠。
为了设置文件描述符我们要使用几个宏.
- FD_SET将fd加入到fdset
- FD_CLR将fd从fdset里面清除
- FD_ZERO从fdset中清除所有的文件描述符
- FD_ISSET判断fd是否在fdset集合中
程序框架大概是:
使用select后我们的服务器程序就变成了.
初始话(socket,bind,listen);
while(1)
{
设置监听读写文件描述符(FD_*);
调用select;
如果是倾听套接字就绪,说明一个新的连接请求建立
{
建立连接(accept);
加入到监听文件描述符中去;
}
否则说明是一个已经连接过的描述符
{
进行操作(read或者write);
}
}
多路复用I/O可以解决资源限制的问题.这模型实际上是将UDP循环模型用在了TCP上面. 这也就带来了一些问题.如由于服务器依次处理客户的请求,所以可能会导致有的客户会等待很久.
UDP
对比TCP模式,客户端和服务端仍然需要首先调用socket()函数获得一个句柄;客户端需要调用bind()函数绑定端口号、IP等信息;客户端则需要connect()函数建立连接。概括而言初始化过程:
客户端:socket->bind
服务端:socket->connect
收发数据方面:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,
struct sockaddr * from int *fromlen)
int sendto(int sockfd,const void *msg,int len,unsigned int flags,
struct sockaddr *to int tolen)
sockfd,buf,len的意义和read,write一样,分别表示套接字描述符,发送或接收的缓冲区及大小.recvfrom负责从 sockfd接收数据,如果from不是NULL,那么在from里面存储了信息来源的情况,如果对信息的来源不感兴趣,
可以将from和fromlen 设置为NULL.sendto负责向to发送信息.此时在to里面存储了收信息方的详细资料.
UDP循环服务器的实现非常简单:UDP服务器每次从套接字上读取一个客户端的请求,处理, 然后将结果返回给客户机.可以用下面的算法来实现.
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}
因为UDP是非面向连接的,没有一个客户端可以老是占住服务端. 只要处理过程不是死循环, 服务器对于每一个客户机的请求总是能够满足.
举一个例子:
/* 服务端程序 server.c */
#define SERVER_PORT 8888
#define MAX_MSG_SIZE 1024
void udps_respon(int sockfd)
{
struct sockaddr_in addr;
int n;
socklen_t addrlen;
char msg[MAX_MSG_SIZE];
while(1)
{ /* 从网络上读,写到网络上面去 */
memset(msg, 0, sizeof(msg));
addrlen = sizeof(struct sockaddr);
n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,(struct sockaddr*)&addr,&addrlen);
/* 显示服务端已经收到了信息 */
fprintf(stdout,"I have received %s",msg);
sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen);
}
}
int main(void)
{
int sockfd;
struct sockaddr_in addr;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s\n",strerror(errno));
exit(1);
}
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr.sin_port=htons(SERVER_PORT);
if(bind(sockfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in))<0)
{
fprintf(stderr,"Bind Error:%s\n",strerror(errno));
exit(1);
}
udps_respon(sockfd);
close(sockfd);
}
客户端程序
/* 客户端程 序 */
void udpc_requ(int sockfd,const struct sockaddr_in *addr,socklen_t len)
{
char buffer[MAX_BUF_SIZE];
int n;
while(fgets(buffer,MAX_BUF_SIZE,stdin))
{ /* 从键盘读入,写到服务端 */
sendto(sockfd,buffer,strlen(buffer),0,addr,len);
bzero(buffer,MAX_BUF_SIZE);
/* 从网络上读,写到屏幕上 */
memset(buffer, 0, sizeof(buffer));
n=recvfrom(sockfd,buffer,MAX_BUF_SIZE, 0, NULL, NULL);
if(n <= 0)
{
fprintf(stderr, "Recv Error %s\n", strerror(errno));
return;
}
buffer[n]=0;
fprintf(stderr, "get %s", buffer);
}
}
int main(int argc,char **argv)
{
int sockfd,port;
struct sockaddr_in addr;
if(argc!=3)
{
fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]);
exit(1);
}
if((port=atoi(argv[2]))<0)
{
fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]);
exit(1);
}
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s\n",strerror(errno));
exit(1);
}
/* 填充服务端的资料 */
bzero(&addr,sizeof(struct sockaddr_in));
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
if(inet_aton(argv[1],&addr.sin_addr)<0)
{
fprintf(stderr,"Ip error:%s\n",strerror(errno));
exit(1);
}
if(connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) == -1)
{
fprintf(stderr, "connect error %s\n", strerror(errno));
exit(1);
}
udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in));
close(sockfd);
}