目录
第一步,创建epoll对象(集合)epoll_create()
前面已经学习了系统编程,本节开始学习网络编程!
网络概述
关于网络编程,推荐书籍:《TCP/IP详解》
Linux网络基础
Linux的优点之一就是在于它丰富而稳定的网络协议栈(实现这种协议的代码),其范围是从协议无关层(如通用的socket层接口和设备层)到各种网络协议的实现;
对于网络理论介绍一般采用OSI模型,但是Linux中网络栈的介绍一般分为四层的Internet模型。
网络模型
OSI模式只是一个理论模式,很少有人用代码实现它;
我们在Linux里面分成TCP/IP四层,其中应用层对应有五种协议:
TFIP和FTP是传文件用的,其中TFIP是加密用的,比FIP更安全;
NFS是网络文件系统,我们往开发板上烧写操作系统(uboot内核和文件系统)的时候,由于文件系统(代码)经常需要改动,如果不停地烧写就很麻烦,那么我们完全可以把文件系统放在电脑上,开发板启动的顺序:在开发板上启动uboot启动内核,内核启动后,通过网线接到电脑上面,然后再电脑上启动文件系统,这种就叫网络文件系统;
Telnet Rlogin是一种登录的协议;
DNS是域名解析协议;
再往下传输层就属于内核里面的东西了,在传输层有两种协议:
TCP协议,UDP协议(面试重点)
然后网络层的协议有三种:IP(和TCP一样都非常重要)、 ARP(地址解析协议,如何根据IP地址得到电脑的地址,每个网络设备都有一个硬件地址,每个设备的IP地址随时可能发生变化,但是它的硬件地址是不会变化的,所以我们在通信的时候最终还是要找到它的硬件地址)、 RARP(逆地址解析,反过来,根据硬件地址找到IP地址)
网络接口层是跟驱动相关的,比如IEEE的一些标准。
补充命令32:ifconfig
这行命令可以查看网络设备的硬件地址
Eth0就是物理网卡的名字,它的地址是192.168.0.163
Lo是loop回环的意思,这个网卡是虚拟的网卡,表示的是本机,所以IP地址是127.0.0.1,这个地址也叫回环IP地址
TCP/IP协议族体系结构
比如当我们用QQ聊天对活框,输入“hello”,点击发送后,这个“hello”字符串就一层一层地经过操作系统,通过网线(或者无线网卡)传出去,这个过程就是从应用层到传输层,再到网络层、网络接口层。用户看到的只是发送的字符串,其实当它出去的时候,这个数据包是非常大的,因为在每一层都加上数据在前面(包头,也就是每一层的标志)。
从底层开始:
数据链路层(关于驱动(硬件)的一些代码,直接跟硬件打交道)
数据链路层实现了网卡接口的网络驱动程序,以处理数据在网络媒介上(比如以太网)上的传输。不同的物理网络层具有不同的电器特性,网络驱动驱动程序隐藏了这些细节,为上层协议提供一个统一的接口。
网络层(选路和转发)
网络层实现数据包的选路和转发。WAN通常使用众多分级的路由器来连接分散的主机或LAN,因此,通信的两台主机一般不是直接连接的,而是通过多个中间节点连接的。网络层的任务就是选择这些节点,以确定两台主机间的通信路径。同时,网络层对上层协议隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信的双发是直接相连的。
比如双方隔着很远的距离,现在用QQ聊天,数据经过很长一段距离的传输,手机连接家里的路由器,家里的路由器再到小区的路由器,小区的路由器再到运营商那边,最终到腾讯的服务器那边,然后再转到聊天的对方手机上。这条过程路径就是网络层选择的一条路径把数据转发出去。
传输层(选择传输的方式,到底是以字节流的方式传输还是以数据包的方式传输)
传输层为两台主机上的应用程序提供端到端(end to end)的通信。与网络层使用的逐跳的通信方式不同,传输层只关心通信的起始端和目的端,而不在于数据包的中转过程。
应用层(应用层负责处理应用程序的逻辑)
应用层协议很多:
ping:应用程序,不是协议,调试网络环境(检测网络通不通);
telnet:远程登录协议;
DNS:机器域名到ip的转换;
HTTP:超文本传输协议(Hypertext transfer protocol)。是一种详细规定了浏览器和万维网(WWW = World Wide Web)服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议;(HTTP的详细解释推荐这篇博文:HTTP(超文本传输协议)的通俗理解_超文本传输协议是什么意思-CSDN博客)
DHCP:动态主机配置协议。(当手机连接路由器的时候,路由器会自动给手机分配一个IP地址,这个过程就由DHCP完成)
数据封装
应用程序数据在发送到屋里网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层协议的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程称为封装。
与数据封装的过程相反,对方收到数据包之后,又一层一层地把头部去掉,最终发送到对方手机上看到的数据就是用户发送的原始数据了。
TCP协议
TCP协议(Transmission Control Protocol,传输控制协议)为应用层提供可靠的、面向连接的、基于流(stream)的服务。TCP协议使用超时重传、数据确认等方式来确保数据包被正确的发送到目的,因此TCP服务是可靠的。使用TCP协议通信的双方必须先建立TCP连接,并且在内核中为该连接维持一些必须的数据结构。当通信结束时,双方必须关闭连接以释放这些内核数据。
“可靠”指的是比如说A发送给B数据,A还要等B回应,当A收到B的回应的时候才认为本次传输已经完成了。如果A发送给B数据没有等到B的回应,它就会认为这个消息传输失败了,它就会重发一遍,这就是“超时重传”。
而“面向连接”指的是A和B正在进行通信之前必须要修建出一条路,也就是把路先铺好。然后它们的数据就在这条路上来回传输。
“基于流”的“流”就像水流一样一个字节一个字节地发送过去。
以上就是TCP协议的特征。
TCP协议头部结构
(网络公司面试可能要画出来)
总共不到100个字节。
“端口”:任意一个网卡都有65536个端口,编号就是从0到65535,每个软件会选择兼并不同的端口,比如,QQ兼并其中两个端口,微信兼并另外三个端口,或者你自己再写一个代码兼并别的1个端口,然后微信在发送数据的时候只会往自己兼并的那三个端口发,QQ发送数据的时候只会往自己兼并的两个端口发,所以微信聊天的时候一定不会被QQ收到,这就是端口的作用,如果没有端口的话就全乱了。
“源端口”:发送端的端口
“目的端口”:要发送数据给到的那个端口
因为有了“面向连接”的服务,所以TCP有了三个握手,四次挥手的概念
TCP三次握手
所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
当数据传输完成之后要断开连接,就要用到四次挥手
TCP四次挥手
TCP连接的删除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
详细解释推荐博文:简单理解TCP三次握手四次挥手(看一遍你就懂)-CSDN博客
UDP协议
UDP协议(User DataGram Protocol,用户数据报协议)它与TCP协议完全相反,提供不可靠、无连接和基于数据报的服务。不可靠意味着UDP协议无法保证数据从发送端正确的传送到接收端。如果数据在中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,则UDP协议只是简单的通知应用层发送失败。因此使用UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑性。
“不可靠”意思就是A给B发送数据的时候,只要A发送出去,传输就算结束了,它不会等B回应,它不管B有没有收到。
“无连接”意思就是在传输数据之前,不需要提前修一条路出来。
“基于数据报”和TCP的“基于流”是相反的,它的意思就把数据打包起来传输过去。
很明显TCP比UDP更好一点,但是UDP为什么没有被淘汰呢?
是因为UDP也有自己的优点,就是它在内核中占的空间比较小。所以在嵌入式设备里面,如果对空间要求比较高,就使用UDP协议。UDP的效率比较高,因为它不需要超时重传这些操作。传输视频的时候我们一般使用UDP协议,视频传输就算中间丢了一两帧,也不影响视频的流畅程度,况且视频传输要求的效率要比较高。
UDP协议头部结构
套接字Socket
Linux中的网络编程通过Socket(套接字)接口实现,socket是一种文件描述符。
注:Linux系统中,“一切皆文件”,这里的“网络”也可以抽象为一个文件,凡是需要用到“网络”的地方,不管是TCP编程还是UDP协议编程,或者其他更上层的一些协议(也是通过TCP/UDP协议完成的),这些协议在使用的时候第一步都是要创建一个socket网络文件,返回值就是一种文件描述符。
套接字socket有三种类型:
流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP保证了数据传输的正确性和顺序性;
数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议UDP。
原始套接字
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议的测试等。
端口号和IP地址
网络里面有两个参数特别重要:端口号和IP地址。
比如A电脑要和B电脑进行通信的话,得根据IP地址找到B电脑,然后通过端口找到对应的程序(因为可能B电脑上有很多程序可能接收网络数据,所以对应的程序是通过端口来区分的)
因此我们需要一个结构体,将端口号和IP地址整合在里面。
地址结构
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
sa_family:
地址族,采用“AF_xxx”的形式,如:AF_INET
sa_data:
14字节的特定协议地址
但是现在已经不太用这个结构体了,我们只要记住这个结构体的名字就可以了。
编程中一般并不直接针对sockaddr数据结构操作,而是使用与sockaddr等价的sockaddr_in数据结构。
我们现在用的结构体是这个:
struct sockaddr_in
{
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址,sin_addr也是一个结构体 */
unsigned char sin_zero[8]; /* 填0 ,这个成员一般用不到*/
};
IP地址的结构体
struct in_addr
{
unsigned long s_addr;
}
s_addr:32位的地址,IP地址是一个字符串,我们后面要转换成长整型。
地址转换
IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的是IP地址是由32位的整数表示的,为了转换我们可以使用下面两个函数:
int inet_aton(const char *cp,struct in_addr *inp);
char *inet_ntoa(struct in_addr in);//把长整型转换成字符串的形式
inet_addr(char *ip);
编程中经常使用第三个函数,可以直接把字符串形式的 IP 地址转换成32位整数表示。
字节序转换
不同类型的 CPU 对变量的字节存储顺序可能不同,分为大端字节序和小端字节序。
小端字节序:低字节存放在低地址,高字节存放在高地址。
大端字节序:高字节存放在低地址,低字节存放在高地址。
而网络传输的数据顺序是一定要统一的。所以当内部字节存储顺序和网络字节顺序不同时,就一定要进行转换。
如果不转的话,比如你想发送个1,而对方收到时这个1放在了高位,就变成了一个很大的数。
网络字节顺序采用大端排序方式。(主机字节序是小端字节序)
字节序转换几个函数:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
因为我们都是电脑对电脑传输数据,并且电脑一般都是小端字节序,所以我们一般不需要考虑字节序转换的问题,但是我们需要用到uint16_t htons(uint16_t hostshort);这个函数。尤其是涉及到跨平台通信的时候,比如电脑想要跟阿里云服务器进行通信的话,端口号必须要通过这个函数转换一下。Htons(h就是host主机字节序,to转换成,n就是net网络字节序,s就是short短整型的意思)
TCP服务器
接下来我们要写两个程序:服务器程序,客户端程序
服务器建立步骤
1、创建一个socket
socket();
2、绑定信息
bind();
上面创建的socket还是个空的文件,现在要往里面填一些东西;填什么?我们用电脑模拟服务器,一般是两个网卡,一个是有线网卡,一个是无线网卡,并且这两个IP地址不一样。我们到底接下来要监听哪个网卡呢?我们就得把IP地址填进去,以及服务器要监听这个网卡上的哪个端口号,因为一个网卡有6万多个端口。因此“绑定信息”就是绑定端口号和IP地址。
服务器在监听,客户端向这个IP地址的端口发起连接,被服务器监听到了,监听到后就可以建立连接,连接起来后就可以进行通信了。
3、监听端口
listen();//设置监听队列
此时服务器在端口那里查询,一旦有人发起连接它就要开始处理,但是它不能同时处理好几个,只能是来一个处理一个,如果同一时刻有很多客户端发起连接,它就会把这些客户端都放在队列里面,然后再挨个去处理
4、接收客户端的连接
accept();
5、读写操作
recv()/send();
也可以用read()/write()这两个函数,参数基本一致。
代码演示:
第一步,创建socket--socket()
一个参数是地址族,我们一般都用ipv4协议填这个:
注:在一些系统中是AF_INET,作用是一样的。
第二个参数是套接字类型,我们现在要写的是TCP协议,我们要选择流式套接字
第三个参数是具体类型,就是具体要使用什么类型,我们直接写0就行,因为我们前面已经指定是SOCK_STREAM了,写0就是默认是TCP协议。
返回值是成功返回一个socket文件描述符,错误的话返回-1
第二步,绑定信息Bind()
第一个参数是socket()的返回值,
第二个参数是一个结构体的地址,我们首先要定义一个这样的结构体并初始化
注意:这个结构体我们不用,我们前面讲过代码中用的是sockaddr_in这个结构体,最后最后取地址之前先要将sockaddr强转成sockaddr_in类型。
定义好结构体之后我们先把这个结构体全部填充为0,然后再初始化(因为一开始都是随机值),用到bzero这个函数,参数是要填充的地址和长度。
在初始化结构体中的端口号时,0~65535都可以选,但是一般不要用前1024个端口号,因为他们一般都被一些知名的软件给占用了。
在初始化结构体中的IP地址时,可以填127.0.0.1,因为这个地址每台电脑都有,向这个IP地址发送数据的话,相当于是只能自己发自己收,一般用于测试的时候。我们就在本机上面测试,所以可以填这个虚拟网卡地址。
如果要对外通信的话可以填这个物理网卡的地址
最后第三个参数是这个结构体的大小。
返回值是失败返回-1
注:(struct sockaddr*)&server_info表示先取地址,再把这个地址的类型转换成另一种地址类型
第三步,设置监听队列listen()
第一个参数是文件描述符sockfd,第二个参数是队列的大小
返回值是错误返回-1
设置好监听队列后程序会停在这里监听......
第四步,接收连接accept()
第一个参数是文件描述符;
第二个参数是一个结构体的地址,有客户端向服务器发送请求,服务器接收的同时还能把客户端的信息记录下来,记在结构体里面;
第三个参数是一个存放了长度的空间的地址;
返回值
一旦accept()接收之后,相当于在服务器和客户端建立了一条通道,这条通道我们叫做TCP连接,TCP连接也是一个文件,所以accept()的返回值是一个文件描述符,接下来的读写操作都是根据这个文件描述符完成的,跟前面的sockfd就没有关系了。如果失败返回-1。
接下来就是从客户端读取数据
第五步,读取数据recv()
这个函数和read()差不多,返回值是实际读到的字节数,失败返回-1
最后读取完数据之后,关闭TCP连接和socket
以上运行结果,当我们在浏览器端(相当于客户端)输入服务器绑定的IP地址和端口号后,服务器就接收客户端的连接了:
或者直接在Linux终端用命令发起连接请求
补充命令33:telnet IP地址 端口号
telnet表示远程连接,这行命令相当于是向这个IP地址的某个端口号发起连接
用这行命令发起连接后,我们发送helloworld,服务器端就收到了这个字符串
这里还提示了断开连接的方式是按住CTRL+中括号
这样就可以断开连接了。
然后服务器这边就死掉了,死循环了,我们按CTRL+C退出进程即可
我们可以在代码上改进一下退出是服务器的状态。
这里如果客户端被异常终止掉了,读不到数据,那recv()会不断地返回0,就不断地死循环,然后不断地换行,打印出来的东西是空的。
我们可以这样修改
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>//bzero的头文件
#include <unistd.h>
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);//地址族:IPV4协议,套接字类型:流式套接字
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;//用于保存服务器的信息:IP,PORT,(还有个地址族)
bzero(&server_info,sizeof(struct sockaddr_in));//清空
server_info.sin_family=PF_INET;//地址族
server_info.sin_port=htons(7000);//端口号,大于1024都行,记得转换字节序
//server_info.sin_addr.s_addr=inet_addr("127.0.0.1");//这个地址每一台电脑都有回环IP地址用于测试,记得将字符串转换成长整形,并且要记得包含头文件
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//对外通信
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)//记得将结构体类型强转一下
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd,10)==-1)//队列大小填10用于测试
{
perror("listen");
exit(3);
}
//程序停在这里监听......
printf("等待客户端的连接...\n");
//接受连接(阻塞),一旦有客户端向服务器发起连接就调用函数接收
struct sockaddr_in client_info;//用于保存客户端的信息
int length=sizeof(client_info);
int fd=accept(sockfd,(struct sockaddr*)&client_info,(socklen_t *)&length);
if(-1==fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n",fd);
//从客户端读取数据
char buf[1024]={0};//从客户端读取数据放在这个数组里面
ssize_t size;
while(1)
{
//接受数据放在buf里面
size=recv(fd,buf,sizeof(buf),0);//从哪读,读到哪,读多少,属性写成0就行
if(size==-1)
{
perror("recv");
break;
}
else if(size==0)
{
break;//跳出循环
}
if(!strcmp(buf,"bye"))//如果收到的是bye就直接退出
break;
printf("%s\n",buf);//打印出接受的数据
bzero(buf,1024);//清空数组
}
close(fd);//关闭TCP连接,不能再接收数据
close(sockfd);//关闭socket,不能再处理客户端的请求
//sockfd用于处理客户端连接 fd用于处理客户端的消息
return 0;
}
这样客户端断开之后,服务器端也就退出了
接下来写客户端的代码
客户端建立步骤
1、创建一个socket
socket();
2、连接服务器
connect(); //发起连接
3、读写操作
recv()/send()
代码演示:
第一步,创建socket--socket()
第二步,发送连接请求connect()
第一个参数是socket函数的返回值,就是通过socket发起连接;
第二个参数是一个结构体的地址,这个结构体和服务器端的sockaddr_in结构体一样,这里是用来保存服务端的信息。
第三个参数是结构体的长度。
返回值失败返回-1
第三步,发送数据send()
第四步,关闭socket--close()
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>//close的头文件
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//发送连接请求
struct sockaddr_in server_info;//保存服务器的信息
bzero(&server_info,sizeof(server_info));
server_info.sin_family=PF_INET;
server_info.sin_port=htons(7000);
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");
if(connect(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)
{
perror("connect");
exit(2);
}
//发送数据
char buf[1024]={0};
while(1)
{
scanf("%s",buf);
if(send(sockfd,buf,strlen(buf),0)==-1)//最后一个参数写成0默认就行
{
perror("send");
break;//跳出循环
}
if(!strcmp(buf,"bye"))
break;
bzero(buf,1024);
}
close(sockfd);//关闭socket
return 0;
}
运行结果,先运行服务器端,然后运行客户端,服务器端接收连接,客户端发送Helloworld,服务器端接收Helloworld,客户端发送bye,服务器和客户端都结束。
总结:
TCP并发服务器
我们之前写的代码是一个服务器只能连接一个客户端,但是实际中一个服务器应该是连接很多个客户端。
所以一个服务器至少要能连接连接两个客户端才有意义
我们将之前的服务器端的代码修改一下,让主线程负责监听客户端发来的连接请求,子线程去负责完成读取客户端的消息。
服务器连接多个客户端
然后实现线程函数
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>//bzero的头文件
#include <unistd.h>
#include <pthread.h>
void *client_recv(void*arg)
{
int fd=*(int*)arg;//获取fd
char buf[1024]={0};//从客户端读取数据放在这个数组里面
ssize_t size;
while(1)
{
//接受数据放在buf里面
size=recv(fd,buf,sizeof(buf),0);//从哪读,读到哪,读多少,属性写成0就行
if(size==-1)
{
perror("recv");
break;
}
else if(size==0)
{
break;//跳出循环
}
if(!strcmp(buf,"bye"))//如果收到的是bye就直接退出
break;
printf("%s\n",buf);//打印出接受的数据
bzero(buf,1024);//清空数组
}
printf("客户端退出 %d\n",fd);
close(fd);
}
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);//地址族:IPV4协议,套接字类型:流式套接字
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;//用于保存服务器的信息:IP,PORT,(还有个地址族)
bzero(&server_info,sizeof(struct sockaddr_in));//清空
server_info.sin_family=PF_INET;//地址族
server_info.sin_port=htons(7000);//端口号,大于1024都行,记得转换字节序
//server_info.sin_addr.s_addr=inet_addr("127.0.0.1");//这个地址每一台电脑都有回环IP地址用于测试,记得将字符串转换成长整形,并且要记得包含头文件
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//对外通信
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)//记得将结构体类型强转一下
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd,10)==-1)//队列大小填10用于测试
{
perror("listen");
exit(3);
}
//程序停在这里监听......
printf("等待客户端的连接...\n");
//服务器的主线程:处理客户端的连接,并且启动一个线程来接收数据
//接受连接(阻塞),一旦有客户端向服务器发起连接就调用函数接收
struct sockaddr_in client_info;//用于保存客户端的信息
int length=sizeof(client_info);
int fd;//为了防止接受数据的时候来不及接受就进入下一轮循环,所以最后把fd放在外面
while(1)
{
fd=accept(sockfd,(struct sockaddr*)&client_info,(socklen_t *)&length);
if(-1==fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n",fd);
//为每一个客户端创建一个线程,从客户端读取数据
pthread_t tid;
if(pthread_create(&tid,NULL,client_recv,&fd)!=0)//从fd从接受数据后返回sockfd
{
perror("pthread_create");
exit(5);
}
pthread_detach(tid);//线程分离,等待和回收线程资源
}
close(sockfd);//关闭socket,不能再处理客户端的请求
//sockfd用于处理客户端连接 fd用于处理客户端的消息
return 0;
}
运行结果,现在可以有很多个客户端给服务器发送消息了,即使客户端退出,服务器还在一直监听。
服务器转发数据
刚才我们写的代码都是客户端给服务器发送数据,但是这在现实生活中是没有意思的,现实生活中要求服务器既能接收多个客户端的消息又能转发某个客户端发来的消息到另一个客户端。
所以我们再把我们上面写的代码完善一下。
我们刚刚写的是子线程收到数据之后就把它打印出来,
现在我们修改成收到数据之后不打印出来而是直接转发出去
我们要创建一个结构体,里面要存放转发的数据和即将要转发的对像的文件描述符
然后我们不再需要之前的用来接收数据的buf
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>//bzero的头文件
#include <unistd.h>
#include <pthread.h>
typedef struct Info//用来读取客户端信息的数据
{
char text[1024];//发什么
int tofd;//转发给谁
}Info;
void *client_recv(void*arg)
{
int fd=*(int*)arg;//获取fd
//char buf[1024]={0};//从客户端读取数据放在这个数组里面
Info i;//定义结构体
ssize_t size;
while(1)
{
//接受数据放在buf里面
size=recv(fd,&i,sizeof(i),0);//从哪读,读到哪,读多少,属性写成0就行
if(size==-1)
{
perror("recv");
break;
}
else if(size==0)
{
break;//跳出循环
}
if(!strcmp(i.text,"bye"))//如果收到的是bye就直接退出
break;
//printf("%s\n",buf);//打印出接受的数据
//转发数据
if(send(i.tofd,&i,size,0)==-1)//发给谁,发什么,发多少,属性写成0
{
perror("send");
break;
}
bzero(&i,sizeof(i));//清空结构体
}
printf("客户端退出 %d\n",fd);
close(fd);
}
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);//地址族:IPV4协议,套接字类型:流式套接字
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;//用于保存服务器的信息:IP,PORT,(还有个地址族)
bzero(&server_info,sizeof(struct sockaddr_in));//清空
server_info.sin_family=PF_INET;//地址族
server_info.sin_port=htons(7000);//端口号,大于1024都行,记得转换字节序
//server_info.sin_addr.s_addr=inet_addr("127.0.0.1");//这个地址每一台电脑都有回环IP地址用于测试,记得将字符串转换成长整形,并且要记得包含头文件
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//对外通信
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)//记得将结构体类型强转一下
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd,10)==-1)//队列大小填10用于测试
{
perror("listen");
exit(3);
}
//程序停在这里监听......
printf("等待客户端的连接...\n");
//服务器的主线程:处理客户端的连接,并且启动一个线程来接收数据
//接受连接(阻塞),一旦有客户端向服务器发起连接就调用函数接收
struct sockaddr_in client_info;//用于保存客户端的信息
int length=sizeof(client_info);
int fd;//为了防止接受数据的时候来不及接受就进入下一轮循环,所以最后把fd放在外面
while(1)
{
fd=accept(sockfd,(struct sockaddr*)&client_info,(socklen_t *)&length);
if(-1==fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n",fd);
//为每一个客户端创建一个线程,从客户端读取数据
pthread_t tid;
if(pthread_create(&tid,NULL,client_recv,&fd)!=0)//从fd从接受数据后返回sockfd
{
perror("pthread_create");
exit(5);
}
pthread_detach(tid);//线程分离,等待和回收线程资源
}
close(sockfd);//关闭socket,不能再处理客户端的请求
//sockfd用于处理客户端连接 fd用于处理客户端的消息
return 0;
}
然后客户端的代码也要修改,客户端既能发送数据也要能接收数据,所以在和服务器建立连接之后,就启动两个子线程,一个负责发送,一个负责接收数据。
然后我们要完成发送数据和接收数据的线程函数
注意我们同样不再是通过buf来接收键值了,这里也要通过一个结构体来存放键值上的消息和想要发送给某个客户端的文件描述符。
最后要回收线程和关闭sockfd
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>//close的头文件
#include <pthread.h>
pthread_t tid[2]={0};//线程号要改为全局的
typedef struct Info
{
char text[1024];
int tofd;
}Info;
void *send_thread(void*arg)
{
int sockfd=*(int*)arg;
Info i;
while(1)
{
scanf("%s %d",i.text,&i.tofd);
if(send(sockfd,&i,sizeof(i),0)==-1)//发到哪,发什么,发多少,属性写0,发到服务器的socket中
{
perror("send");
break;//跳出循环
}
if(!strcmp(i.text,"bye"))
{
pthread_cancel(tid[1]);//如果获取了bye就结束线程,跳出循环前也要取消接收线程
break;
}
bzero(&i,sizeof(i));
}
}
void*recv_thread(void*arg)
{
int sockfd=*(int*)arg;
Info i;
ssize_t size;
while(1)
{
size=recv(sockfd,&i,sizeof(i),0);//从服务器中的socket接收数据
if(size==-1)
{
perror("recv");
break;
}
else if(size==0)
{
break;
}
printf("\t\t%s\n",i.text);
bzero(&i,sizeof(i));
}
}
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//发送连接请求
struct sockaddr_in server_info;//保存服务器的信息
bzero(&server_info,sizeof(server_info));
server_info.sin_family=PF_INET;
server_info.sin_port=htons(7000);
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");
if(connect(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)
{
perror("connect");
exit(2);
}
//启动两个线程
//pthread_tid[2]={0};线程号要改为全局的
if(pthread_create(&tid[0],NULL,send_thread,&sockfd)!=0||
pthread_create(&tid[1],NULL,recv_thread,&sockfd)!=0)
{
perror("pthread_create");
exit(1);
}
//回收线程
void*status;
pthread_join(tid[0],&status);
pthread_join(tid[1],&status);
close(sockfd);//关闭socket
return 0;
}
运行结果:
首先客户端可以自己给自己发,因为它是往sockfd中发送的,所以返回来给自己
这样连接到服务器的所有客户端都是可以互相发送消息的了
UDP服务器
UDP服务器和客户端的代码比TCP的简单很多,因为它不需要客户端connect,服务器也不需要listen和accept。
服务器建立步骤
1、创建socket
socket();
2、绑定信息
bind();
3、收发数据
recvfrom()/sendto();
代码演示:
先创建socket和绑定信息
第一步:创建socketsocket()
第二步:绑定信息bind()
第三步,收发数据recvfrom()/sendto()
然后收发数据,收发数据用recv()不行了,我们要用recvfrom()这个函数
第一个参数是从哪接收数据的文件描述符sockfd
第二个参数是收到的数据放到哪,显然我们需要定义一个数组来存放接收到的数据
第三个参数是接收的字节数
第四个参数直接写0就行
第五个参数是结构体的地址,服务器在接收客户端数据的同时还可以记录下客户端的信息,因此这个结构体就类似TCP协议中的accept的第二个参数,用于保存客户端的信息的。
第六个参数是结构体的大小的地址。
返回值是接收到的字节数,失败返回-1
因为收到客户端发来的IP地址是被转成了长整形,所以我们记录下来后,要打印出来时要转换回字符串,要用到这个函数
这个函数的参数是一个struct in_addr类型的结构体,这个结构体我们之前讲过
这个结构体里面的成员就是IP地址,因此我们只需要将client_info.sin_addr这个结构体转换成字符串类型的就可以了。
Inet_ntoa(client_info.sin_addr)
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
int main()
{
//创建socket 数据报套接字
int sockfd=socket(PF_INET,SOCK_DGRAM,0);
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;
bzero(&server_info,sizeof(server_info));
server_info.sin_family=PF_INET;
server_info.sin_port=htons(6000);//要转换字节序
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//要转换成长整形
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)
{
perror("bind");
exit(2);
}
//收发数据
ssize_t size;
char buf[1024]={0};
struct sockaddr_in client_info;//记录客户信息
int len=sizeof(client_info);
while(1)
{
size=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&client_info,(socklen_t *)&len);
if(-1==size)
{
perror("recvfrom");
break;
}
//把记录下来的客户端信息打印出来
printf("收到 %s:%d的数据 %s\n",inet_ntoa(client_info.sin_addr),client_info.sin_port,buf);
bzero(buf,1024);
}
return 0;
}
客户端建立步骤
1、创建socket
socket();
2、收发数据
recvfrom()/sendto();
这里发送数据的时候不能用send(),要用到sendto()这个函数
第一个参数是要发送到哪里的文件描述符sockfd
第二个参数是发送什么内存,即从键盘上获取数据
第三个参数是发送的字节数
第四个参数直接写0
第五个参数一个结构体的地址,我们发送数据就必须要指定服务器的IP地址和端口号,这就需要一个结构体来存放这些信息发送出去。
第六个参数是这个结构体的长度
返回值是实际发送的字节数,失败返回-1
第一步,创建socket--socket()
首先创建socket
然后发送数据
第二步,收发数据recvfrom()/sendto()
第三步,关闭sockfd--close()
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_DGRAM,0);
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//发送数据
char buf[1024]={0};
struct sockaddr_in server_info;
server_info.sin_family=PF_INET;
server_info.sin_port=htons(6000);
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");
while(1)
{
scanf("%s",buf);
ssize_t size=sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&server_info,sizeof(server_info));
if(-1==size)
{
perror("sendto");
break;
}
bzero(buf,1024);
}
close(sockfd);
return 0;
}
运行结果:
问题来了,当我们再打开一个终端运行客户端代码后,也能给服务器发信息
TCP和UDP的区别
为什么使用TCP协议的服务器,它必须要加多线程才能连接多个客户端,而UDP却不需要呢?
这就是TCP和UDP的区别,跟服务器和客户端需不需要连接是有关系的。
通过下图可以就能看出两者的区别
UDP服务器是一种循环服务器,一次只干一件事情,循环一次就处理一件事情。
而加了多线程的TCP服务器它可以干多件事情。这主要就是他们有无需要连接导致的。
高并发服务器select()/poll()
我们再看这张对比图
TCP服务器也叫并发服务器,UDP服务器也叫循环服务器,服务器就主要分为这两种。
我们来了解一下CS架构:C就是client,客户端,S就是server,服务器端,C端要注重界面的设计,S端要注重的是稳定性和并发,也就是要能在同一时刻稳定地处理多个线程。
注意:虽然TCP服务器可以启动多线程,效率也比较高,但是TCP服务器不能不限度地启动线程,当线程数量达到一定量的时候,服务器就会崩溃。
下面我们就研究一下如何用TCP协议来实现循环服务器的功能。
先来了解一个概念:多路复用技术,它主要用来处理高并发。
多路复用要用到select和poll函数(这个函数和select差不多),以及epoll技术。
select的作用:监听文件描述符,在linux系统中,“一切皆文件”,所以一切都有文件描述符,则select就可以监听一切,这个函数非常重要!
如果将select应用到TCP服务器中?
Select其实是一个集合,把要监听的文件描述符放在这个集合里面,select就在这个集合里面循环监听某个文件描述符可不可读/写,还可以监听他们是否异常发生。
比如select一旦监听到sockfd可读(就把sockfd留在集合里面,把另外的删除),就表示有客户端向服务器发起连接了,就调用处理连接的那个函数。如果监听到fd可读,就说明有客户端向服务器发消息了,就调用接收的函数。
显然我们需要两个集合,一个用来装文件描述符(起备份作用),一个用来循环监听(起临时操作作用)。
通过select我们就可以实现UDP协议在等待客户端给它发信息时的循环监听功能。
select使用步骤
1. 设置要监控的文件
2. 调用Select开始监控
3. 判断文件是否发生变化
代码演示:
第一步,设置要监控的文件
什么是集合?这里的集合其实指是这个类型,可以把它理解成一个数组。
我们首先在服务器等待客户端连接这里定义两个集合,readset, tmpset
我们可以用这些宏函数操作集合
void FD_CLR(int fd, fd_set *set);把该fd从该集合中删除;
int FD_ISSET(int fd, fd_set *set);判断该fd是否还留在该集合里面;
void FD_SET(int fd, fd_set *set);把该fd添加到该集合里面;
void FD_ZERO(fd_set *set);清空该集合。
初始化集合用的是清空
然后把文件描述符添加到集合里面,在客户端没有发起连接之前只有sockfd
注意,我们还要记录目前最大的文件描述符,因为select的第一个参数是nfds文件描述符的个数(包含进程打开时默认的三个文件描述符0 1 2),所以这个参数我们一般都填最大的那个文件描述符+1,此时我们的sockfd是3,所以这整个进程中的文件描述符的个数就是3+1,0到3就是4个文件描述符。
然后通过select循环监听
第二步,调用Select开始监控
Select函数
第一个参数是进程中文件描述符的个数;
- 三、四个参数都是集合,如果我们把这个集合作为第二个参数则表示我们要监听这个文件描述符是否可读,如果作为第三个参数则表示我们要监听这个文件描述符是否可写,如果作为第四个则表示要监听它是否有异常发生;
我们这里只要监听是否可读就行,其他两个参数写成空。注意,在select前要备份集合,因为select等会儿会清除掉不监听的文件描述符。
最后第五个参数是一个时间结构体的地址,一个是秒,一个微秒。
填这个时间表示什么?
比如我们把这个参数填成3s,也就是说select最多在那等3s,3s之内如果有数据可读的话,程序就继续往下走,如果3s过后没有数据可读就不等了,程序继续往下走。
我们这里直接写成NULL空,表示让它一直在那里阻塞,一直让它在那边等着,等到有数据可读,程序再往下走,如果没有数据可读就一样让它停在那边。
Select的返回值是实际可读的文件描述符的个数,失败返回-1。
接下来如果程序能往下走的话说明有文件可读了,我们就需要接收连接或者读取数据
第三步,判断文件是否发生变化
先判断是不是客户端发起的连接,如果是我们就调用accept接收连接
如果不是,则表示是客户端发来的数据,那么我们就要调用recv接收数据
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>//bzero的头文件
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);//地址族:IPV4协议,套接字类型:流式套接字
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;//用于保存服务器的信息:IP,PORT,(还有个地址族)
bzero(&server_info,sizeof(struct sockaddr_in));//清空
server_info.sin_family=PF_INET;//地址族
server_info.sin_port=htons(7000);//端口号,大于1024都行,记得转换字节序
//server_info.sin_addr.s_addr=inet_addr("127.0.0.1");//这个地址每一台电脑都有回环IP地址用于测试,记得将字符串转换成长整形,并且要记得包含头文件
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//对外通信
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)//记得将结构体类型强转一下
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd,10)==-1)//队列大小填10用于测试
{
perror("listen");
exit(3);
}
//程序停在这里监听......
printf("等待客户端的连接...\n");
//定义两个集合
fd_set readset,tmpset;
//将两个集合初始化
FD_ZERO(&readset);
FD_ZERO(&tmpset);
//把文件描述符添加到集合
FD_SET(sockfd,&readset);
//记录目前最大的文件描述符
int maxfd=sockfd;
//开始循环监听
int ret,i;
int fd[1024]={0};
while(1)
{
tmpset=readset;//备份集合
ret=select(maxfd+1,&tmpset,NULL,NULL,NULL);
if(-1==ret)
{
perror("select");
break;
}
//接下来如果程序能往下走的话说明有文件可读了
//判断sockfd是否留在集合中,有则表示可读,也就是有客户端发起连接
if(FD_ISSET(sockfd,&tmpset))
{
for(i=0;i<1024;i++)//找出一个合适的fd(后面我们会把下线后的文件描述符归0,在这里可以找出归0的fd重复利用)
{
if(0==fd[i])
break;
}
//接受连接(阻塞)
struct sockaddr_in client_info;//用于保存客户端的信息
int length=sizeof(client_info);
fd[i]=accept(sockfd,(struct sockaddr*)&client_info,(socklen_t *)&length);
if(-1==fd[i])
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n",fd[i]);
//把新的文件描述符加入集合中
FD_SET(fd[i],&readset);
//更新最大文件描述符,只有当此时的fd不是被重复利用的fd,才需要更新
if(maxfd<fd[i])
{
maxfd=fd[i];
}
}
else//是fd留在里面,则表示有客户端发送数据,定义一个fd数组区分每个fd
{
//判断是哪个文件描述符
for(i=0;i<1024;i++)
{
if(FD_ISSET(fd[i],&tmpset))//判断一下文件描述符i在不在里面,如果不在就++
{
//如果是
//从客户端读取数据
char buf[1024]={0};//从客户端读取数据放在这个数组里面
ssize_t size;
while(1)
{
//接受数据放在buf里面
size=recv(fd[i],buf,sizeof(buf),0);//从哪读,读到哪,读多少,属性写成0就行
if(size==-1)
{
perror("recv");
}
else if(size==0)
{
printf("客户端 %d 退出\n",fd[i]);
FD_CLR(fd[i],&readset);//从集合中删掉
close(fd[i]);//关闭TCP连接
fd[i]=0;//fd归0,留着重复利用
break;//跳出循环
}
else
{
printf("%s\n",buf);//打印出接受的数据
}
break;
}
}
}
}
}
close(sockfd);//关闭socket,不能再处理客户端的请求
//sockfd用于处理客户端连接 fd用于处理客户端的消息
return 0;
}
运行结果:
这就是用select来实现多路复用技术模仿UDP循环监听的功能,用TCP协议也能做成的一个循环服务器。这样我们就不用多线程也能用TCP协议完成一个服务器连接多个客户端的要求。
高并发服务器epoll技术
我们上面用select完成的代码其实是存在一点缺陷的。
就是这里虽然能判断出有客户端发送数据,但它并不能感知是谁发送的数据,所以我们要循环了1024次找出这个客户端,如果真的有1024个客户端的话,那它的效率就很低了。
而且select还有一个致命的缺陷就是它的内存已经把集合的容量给限制死了(通过一个宏定义来规定的,在linux里面是1024),就是有并发的上限,最多只能容纳1024个文件描述符,即1024个客户端。
能不能去掉这个1024的上限?
我们可以用epoll这个技术来实现。epoll不是一个函数,是很多个函数都以epoll开头的。
它能解决容量限制的问题,并且能让可读的那个文件描述符在集合中自己“蹦出来”,不用我们去找。
在epoll集合中存放的不再是文件描述符了,它存放的是“事件”。所谓的事件就是用一个结构体来封装几个成员,其中一个成员就是文件描述符。
代码演示:
我们也是在服务器正在等待客户端的连接时开始加入epoll技术
第一步,创建epoll对象(集合)epoll_create()
首先创建epoll对象(即在创建集合),这个epoll是使用树形结构来实现的。
用到epoll_create()函数
它只有一个参数,在以前的Linux中表示的是这个epoll的容量有多大,在现在的linux中这个size已经没有意义了,只要填的数大于0就行了。我们就直接写1即可。
返回值是一个文件描述符,失败返回-1
在客户端还没有和服务器端建立连接的时候,目前就一个sockfd,所以我们就将sockfd封装成一个事件,添加到集合中
第二步,封装事件添加到集合epoll_ctl()
用到epoll_ctl()这个函数
第一个参数是epoll_create函数的返回值epfd,就是要往epfd这个集合里面添加;
第二个参数可以填写成这几个:
EPOLL_CTL_ADD是往集合里面添加事件,EPOLL_CTL_MOD是修改集合里面的事件,EPOLL_CTL_DEL是删除集合里面的事件。
第三个参数是要添加的文件描述符是哪一个。
第四个参数是表示事件的结构体的地址:
注:epoll_data_t这个联合体里面我们主要用到的是int fd这个成员就行。
结构体的第一个成员__uint32_t events;表示的是事件的属性,可指明是可读还是可写
EPOLLIN表示可读,EPOLLOUT表示可写。
还可以指明触发方式,有边沿触发(从无数据到有数据的这个瞬间)和电平触发(有数据的时候)。我们目前没有涉及到这个触发方式,但是可以或上这个EPOLLET,也可以不加,默认是水平触发(电平触发)。
epoll_ctl()的返回值,失败返回-1
第三步,开始监听epoll_wait()
接下来开始监听
用到函数epoll_wait()
第一个参数是前面得到的epfd,指定监听哪个集合。
第二个参数是一个结构体的地址,我们要定义一个结构体数组,用来存放满足条件的事件。集合中同时有多个事件可读时,它会把这些事件都放在这个事件结构体数组当中,然后接下来处理这个数组就行了。数组的大小可以自己定义。
第三个参数是最多“跳出”几个事件,结构体数组大小定义了几个,就是能跳出几个事件。
第四个参数是暂停的时候,直接写-1,表示阻塞,程序会一直停在这边,直到有事件可读才会向下走。
返回值是有多少个事件满足条件,如果失败返回-1。
第四步,接收请求并处理
接下来是处理操作
如果是客户发起连接请求则接收连接
如果是客户发送数据则接收数据
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>//inet_addr的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>//bzero的头文件
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
//创建socket
int sockfd=socket(PF_INET,SOCK_STREAM,0);//地址族:IPV4协议,套接字类型:流式套接字
if(-1==sockfd)
{
perror("socket");
exit(1);
}
//绑定信息
struct sockaddr_in server_info;//用于保存服务器的信息:IP,PORT,(还有个地址族)
bzero(&server_info,sizeof(struct sockaddr_in));//清空
server_info.sin_family=PF_INET;//地址族
server_info.sin_port=htons(7000);//端口号,大于1024都行,记得转换字节序
//server_info.sin_addr.s_addr=inet_addr("127.0.0.1");//这个地址每一台电脑都有回环IP地址用于测试,记得将字符串转换成长整形,并且要记得包含头文件
server_info.sin_addr.s_addr=inet_addr("192.168.0.163");//对外通信
if(bind(sockfd,(struct sockaddr*)&server_info,sizeof(server_info))==-1)//记得将结构体类型强转一下
{
perror("bind");
exit(2);
}
//设置监听队列
if(listen(sockfd,10)==-1)//队列大小填10用于测试
{
perror("listen");
exit(3);
}
//程序停在这里监听......
printf("等待客户端的连接...\n");
//创建epoll对象(创建集合)
int epfd=epoll_create(1);
if(-1==epfd)
{
perror("epoll_create");
exit(4);
}
//把socket封装事件,添加到集合中
struct epoll_event ev;
ev.events=EPOLLIN|EPOLLET;//事件属性,可读,水平触发
ev.data.fd=sockfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev)==-1)
{
perror("epoll_ctl");
exit(5);
}
//开始监听和处理
while(1)
{
//监听
struct epoll_event events[10];//用于存放满足条件的事件
int num=epoll_wait(epfd,events,10,-1);
if(num==-1)
{
perror("epoll_wait");
break;
}
//处理
int i;
for(i=0;i<num;i++)
{
if(events[i].data.fd==sockfd)//有客户端发起连接
{
//接受连接(阻塞)
struct sockaddr_in client_info;//用于保存客户端的信息
int length=sizeof(client_info);
int fd=accept(sockfd,(struct sockaddr*)&client_info,(socklen_t *)&length);
if(-1==fd)
{
perror("accept");
exit(4);
}
printf("接受客户端的连接 %d\n",fd);
//把fd添加到集合中
ev.events=EPOLLIN;//事件属性,可读
ev.data.fd=fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)//把fd添加到epfd这个集合中
{
perror("epoll_ctl");
exit(5);
}
}
else//有客户端发送数据
{
//从客户端读取数据
char buf[1024]={0};//从客户端读取数据放在这个数组里面
ssize_t size;
//接受数据放在buf里面
size=recv(events[i].data.fd,buf,sizeof(buf),0);//从哪读,读到哪,读多少,属性写成0就行
if(size==-1)
{
perror("recv");
}
else if(size==0)
{
//把事件从集合中删除
if(epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&events[i])==-1)
{
perror("epoll_ctl");
exit(5);
}
close(events[i].data.fd);//关闭退出的fd
printf("客户端 %d 退出\n",events[i].data.fd);
}
else
{
printf("%s\n",buf);//打印出接受的数据
}
}
}
}
close(sockfd);//关闭socket,如果epoll_wait出错就会执行到这句
//sockfd用于处理客户端连接 fd用于处理客户端的消息
return 0;
}
大概流程:
第一次进入while循环的时候,epollwait监听到的满足条件的文件描述符只有sockfd,返回值num等于1,epollwait就把sockfd存放到了events数组中,然后执行for循环中的if (events[i].data.fd == sockfd) ,在这个if里面accept产生了fd,并且把这个fd覆盖了sockfd,然后i++,i=1,i不小于num,跳出for循环;
又进入下一轮while,此时epollwait监听到的是fd符合条件,存放进events数组中,num=1,然后进入for循环这次执行的是里面的else分支接收客户的数据,就直接从events数组中recv,接收完之后再删除这个fd,i++,i=1,i不小于num跳出for;
进入下一轮while循环,此时epollwait没有监听到sockfd和fd了,由于epollwait函数是一个阻塞的函数,它会一直在监听等待别的客户端或者等待这个客户端再跟它连接。
运行结果:
并发服务器总结(C++岗位面试重点)
1.TCP+多线程:结构体简单,线程太多对服务器硬件要求比较高(单核的CPU能用单线程就不要用多线程,耗时的任务或者阻塞的任务才考虑用多线程解决)。
2.UDP:结构体简单,对服务器性能要求不高,处理的效率比较低(尤其是不够快的CPU)。
3.select:简单,容易理解,并发的数量有限(最多1024个),处理的效率比较低(需要循环遍历所有文件描述符,找到满足条件的)。
4.poll:简单,容易理解,并发数量没有限制,处理的效率比较低(需要循环遍历所有文件描述符,找到满足条件的)。
5.epoll:并发数量没有限制,处理的效率比较高(不需要循环遍历所有文件描述符),但是对于耗时的任务还需要完善(它需要完成接收数据的任务才可以回来继续监听)。
6.epoll+多线程:缺点就是频繁启动线程导致效率降低。
7.epoll+线程池:能解决epoll+多线程服务器的缺点
8.libevent:底层用的是epoll实现的,之后会详细讲解。
下节开始学习Linux常用库!
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓