目录
一、网络协议模型
OSI七层模型
OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
应用层:提供应用服务
表示层:数据的表示和加密·
会话层:建立新会话
传输层:约束传输方式
网络层:实现跨子网通信
数据链路层:实现同一子网内数据交换
物理层:约束一些物理标准
其中,路由器工作在OSI七层协议模型的第三层,即网络层(网际层)的设备,主要用于组网(跨子网通信)、对数据进行转发(路由)、维护路由表。
OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,从下到上分别是接口层、网络层、传输层和应用层,这就是TCP/IP 模型。
在TCP/IP模型中,网络层和运输层之间的区别是最为关键的:网络层( IP)提供点到点的服务,而运输层(TCP和UDP)提供端到端的服务。
TCP、UDP协议属于传输层、IP协议属于网络层(互联网层)
TCP/UDP模型
TCP(传输控制协议)和UDP(用户数据报协议)
TCP协议是一种面向连接的协议,可靠性高,能丢包重发,有丰富的校验机制;
UDP协议是一种面向报文的协议,可靠性差,需依靠应用层提高可靠性。
二、IP地址
IP地址的结构: 网络号 + 主机号 ,根据结构不同分为ABCDE五类。
IP地址需和子网掩码配套使用,单独说IP地址没有任何意义。
网络号相同的IP,说明在同一个子网内。
子网掩码是用来划分子网的,是一个32bit的二进制数,用1来掩网络号,用0来掩主机号。
三、字节序
在进行网络通信时,需要注意字节序不同的问题
大端字节序-------大端序---------网络字节序
小段字节序-------小端序---------主机字节序
小端序:低字节存低位
大端序:低字节存高位
ps:检测自己主机的字节序,可以通过一个共用体,内部定义一个数据和一个数组的方式来检查。
四、TCP CS模型的搭建
服务器分类
1、重复型(循环型)
①等待一个客户端请求的到来
②处理客户端请求
③发送响应给发送请求的客户端
④重新回到步骤①
2、并发型
①等待一个客户端请求的到来
②启动一个新服务器来处理这个客户端请求。在这个新服务器中可能开辟新的进程、线程,生成新的任务,具体如何操作则依赖于底层操作系统,在对客户端服务完毕后,这个新开启的服务端关闭
③重复进行①的操作
总结:在并发型服务器的架构下,每个客户端都有一个与之对应的服务端,也就可以同时为多个客户端服务。
常用函数接口介绍
1、socket
目的:创建一个通信端口
int socket(int domain, int type, int protocol)
参数domain:指定所使用的协议族,像IPV4、IPV6协议族
参数type:套接字的类型,像流式套接字、数据报套接字
参数protocol:协议,一般是给0,表示默认
2、bind
目的:给socket绑定IP和端口
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
参数sockfd:有socket创建的文件描述符,即待绑定的socket
参数sockaddr *addr:这是一个结构体首地址,里面存储着ip号和端口的首地址
参数socklen_t addrlen:参数二指向的结构体的大小
需要注意的是bind函数的第二个参数所用的类型是struct sockaddr,而Linux为TCP通信所提供的结构体类型是struct sockaddr_in,因此在作为bind函数的入参时,需要进行一次数据类型的强制转换。这一点在后面的实际程序代码实现中会有体现。
3、listen
目的:可理解为创建了一个监听队列,来标记客户端的连接请求,如果队列已经满了。则客户端会收到连接被拒的错误。
int listen(int sockfd, int backlog)
参数sockfd:监听套接字的文件描述符,经由socket创建,bind绑定后的那个套接字
参数backlog:监听队列的大小
listen函数一般用于bind绑定之后,accept监听之前
4、accept
目的:接收一个套接字的连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数sockfd:由listen监听过后产生的监听套接字的文件描述符
参数addr:指向一个结构体的指针,结构体用于存储对端的数据
参数addrlen:参数2所指向的结构体的字节大小长度
返回值:成功返回通信套接字的文件描述符,失败返回-1和错误码
5、send
目的:发送数据,通过socket
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
参数sockfd:要进行通信的通信套接字的文件描述
参数buf:要进行发送的数据的首地址
参数len:要发送的数据的长度
参数flags:默认给0
6、recv
目的:接收数据,通过socket
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
参数socketfd:要进行通信的通信套接字文件描述符
参数buf:用于存储接收到的数据的内存首地址
参数len:要接收的数据长度(字节数)
参数flags:默认给0
7、connect
目的:发起一个socket的连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数sockfd:客户端socket函数创建的套接字文件描述符。
参数addr:这是一个结构体首地址,里面存储着服务器的ip号和端口的首地址。
参数addrlen:参数2指向的结构体所占的内存空间大小。
服务器与客户端搭建的基本流程
1、TCP服务器的搭建流程及代码
socket -> bind -> listen -> accept -> send/recv -> close
2、TCP客户端的搭建流程及代码
socket -> connect -> send/recv -> close
客户端其实并没有什么值得深究的,此处给出的客户端代码是通用的,在文章后面的与服务端之间的通信实验中,都采用的是这个客户端程序。
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
//socket
int clifd = -1;
clifd = socket(AF_INET, SOCK_STREAM, 0);
if(clifd < 0)
{
puts("socket error.");
return -1;
}
puts("socket success.");
//connect
struct sockaddr_in myser;
myser.sin_family = AF_INET;//采用ipv4机制
myser.sin_port = htons(7777);//端口号设为7777
myser.sin_addr.s_addr = inet_addr("127.0.0.1");//采用本地回环地址
int ret = connect(clifd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("connect error.");
close(clifd);
return -1;
}
puts("connect success.");
//send or recv
char buf[50];
memset(buf, 0, sizeof(buf));
gets(buf);
send(clifd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
ret = recv(clifd, buf, sizeof(buf), 0);
if(ret > 0)
{
puts(buf);
}
//close
close(clifd);
return 0;
}
五、循环服务器的搭建
循环服务器指的是,一个服务器通过分时,处理多个客户端,只有当前一个客户端处理完毕,下一个客户端才开始处理。
1、阻塞的循环服务器
//伪代码
socket //创建监听套接字
bind //绑定IP和端口号
listen //开启监听
while(1)
{
connfd = accept //接受客户端请求
do_work //执行相关操作
close connfd //关闭客户端通信套节字
}
close listenfd //关闭服务端监听套接字
详细代码展示
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
int accept_cli(int listenfd);
int do_work(int connfd);
int main()
{
int fd = -1;
//创建一个套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
puts("socket fail");
return -1;
}
puts("socket success");
int ret = -1;
struct sockaddr_in myser; //现有的结构体类型是这个,后面需要进行一次强制的类型转换
myser.sin_family = AF_INET;
myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
myser.sin_addr.s_addr = htonl(INADDR_ANY);
//给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("bind fail");
close(fd);
fd = -1;
return -1;
}
puts("bind success");
//进行监听,最大可监听5个
ret = listen(fd, 5);
if(ret != 0)
{
puts("listen fail");
close(fd);
fd = -1;
return -1;
}
puts("listen start");
while(1)
{
int connfd = accept_cli(fd);
if(connfd < 0)
{
puts("no client connection");
continue;
}
do_work(connfd);
close(connfd);//关闭通信套接字,因为与其的服务已经结束完成
}
close(fd);
}
//自定义函数部分
int accept_cli(int listenfd)
{
if(listenfd < 0)
{
puts("lisenfd < 0");
return -1;
}
int connfd = -1;
struct sockaddr_in mycli;
int len = sizeof(mycli);
connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
return connfd;
}
int do_work(int connfd)
{
if(connfd < 0)
{
puts("connfd < 0, cannot do work.");
return -1;
}
char buf[50];
memset(buf, 0, sizeof(buf));
int ret = recv(connfd, buf, sizeof(buf), 0);
if(ret > 0)
{
puts(buf);
send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
}
else if(ret == 0)
{
return -1;
}
return 0;
}
处理效果展示
结果分析
可以看出,阻塞的循环服务器,在处理客户端的任务时。
其循环特性体现在,遵循一个先连接先处理的原则,只有当前一个处理好了才会进行下一个的处理。
其阻塞特性体现在,服务端在没有收到客户端的回应时,其不进行结束,而是一直等待。
2、非阻塞的循环服务器(利用fcntl函数)
//伪代码
socket //创建监听套接字
bind //绑定IP和端口号
listen //开启监听
//设置监听套接字为非阻塞
int flags = fcntl(listenfd, F_GETFL)
fcntl(listenfd, F_SETFL, flags | O_NONBLOCK)
while(1)
{
connfd = accept //接受客户端请求
do_work //执行相关操作
close connfd //关闭客户端通信套节字
}
close listenfd //关闭服务端监听套接字
由于,在进行读写操作时,recv()、send()、accept()等函数都是阻塞进行的,如果所需的资源没有准备好,他们将会一直等待,直到资源被准备好。
所幸,fcntl()函数可以获得和改变已经打开的文件的性质,通过fcntl()函数可以设置文件的属性为非阻塞。
注:Linux下的socket是一种特殊的文件,其也可以用fcntl()函数进行操作
实现原理
将服务端的监听套接字设置成非阻塞的,通过其循环不断地监听客户端的通信套接字,读取不到,就直接下一个,不做停留,也就不会阻塞,直到有一个通信套接字被读取成功,才进行通信,否则其一直在监听通信套接字。
详细代码展示
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
int accept_cli(int listenfd);
int do_work(int connfd);
int main()
{
int fd = -1;
//创建一个套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
puts("socket fail");
return -1;
}
puts("socket success");
int ret = -1;
struct sockaddr_in myser; //现有的结构体类型是这个,后面需要进行一次强制的类型转换
myser.sin_family = AF_INET;
myser.sin_port = htons(8888);//设置端口号,5000以上可以随意设置,5000以下的被使用了
myser.sin_addr.s_addr = htonl(INADDR_ANY);
//给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("bind fail");
close(fd);
fd = -1;
return -1;
}
puts("bind success");
//进行监听,最大可监听5个
ret = listen(fd, 5);
if(ret != 0)
{
puts("listen fail");
close(fd);
fd = -1;
return -1;
}
puts("listen start");
//改变监听套接字的属性为非阻塞,之一步是搭建非阻塞循环服务器的关键
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
while(1)
{
int connfd = accept_cli(fd);
if(connfd < 0)
{
puts("no client connection");
sleep(1);
continue;
}
do_work(connfd);
close(connfd);//关闭通信套接字,因为与其的服务已经结束完成
}
close(fd);
}
//自定义函数部分
int accept_cli(int listenfd)
{
if(listenfd < 0)
{
puts("lisenfd < 0");
return -1;
}
int connfd = -1;
struct sockaddr_in mycli;
int len = sizeof(mycli);
connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
return connfd;
}
int do_work(int connfd)
{
if(connfd < 0)
{
puts("connfd < 0, cannot do work.");
return -1;
}
char buf[50];
memset(buf, 0, sizeof(buf));
int ret = recv(connfd, buf, sizeof(buf), 0);
if(ret > 0)
{
puts(buf);
send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
}
else if(ret == 0)
{
return -1;
}
return 0;
}
其中,在为socket套接字绑定IP时,用到INADDR_ANY,INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
当然,不用INADDR_ANY也可以,可以通过直接指定的IP来给IP值
运行效果展示
结果分析
通过代码,可以看到的是,我们是在服务端accept之前设置socket的属性为非阻塞。
也就是说,在执行accept函数时,将不会再阻塞,一旦没有接收到来自客户端的连接请求,accept函数就会返回,并进行下一次的accept。
通过运行结果,可以看到的是,在没有客户端进行连接的时候,accept函数直接就进行了返回,而阻塞服务器那,accept函数一直在那等着,也就是我们所说的阻塞。
阻塞:函数在没有得到结果前,不会返回,而是会一直等待,直到接收到结果。
非阻塞:函数如果没有得到结果,直接返回,不会一直等待。
六、并发服务器的搭建
循环服务器是分时共享服务器资源,而并发服务器可以在同一时刻,让多个客户端访问。
1、多进程并发服务器
//伪代码
socket
bind
listen
while(1)
{
connfd = accept()
pid_t pid = fork();
if(pid == 0)
{
do_work()
close(connfd)
exit()
}
}
close listenfd
实现原理
服务端在接受一个客户端的请求后,通过开辟一个子进程去处理这个客户端的相关事宜,而在主进程中继续保持对客户端的监听。
详细代码展示
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int accept_cli(int listenfd);
int do_work(int connfd);
int main()
{
int fd = -1;
//创建一个套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
puts("socket fail");
return -1;
}
puts("socket success");
int ret = -1;
struct sockaddr_in myser; //现有的结构体类型是这个,后面需要进行一次强制的类型转换
myser.sin_family = AF_INET;
myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
myser.sin_addr.s_addr = htonl(INADDR_ANY);
//给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("bind fail");
close(fd);
fd = -1;
return -1;
}
puts("bind success");
//进行监听,最大可监听5个
ret = listen(fd, 5);
if(ret != 0)
{
puts("listen fail");
close(fd);
fd = -1;
return -1;
}
puts("listen start");
while(1)
{
int connfd = accept_cli(fd);
if(connfd < 0)
{
continue;
}
pid_t pid = fork(); //开辟子进程进行对客户端的具体处理
if(pid == 0) //等于0的是子进程
{
do_work(connfd);
close(connfd);
exit(0); //处理完后子进程退出
}
}
close(fd);
}
//自定义函数部分
int accept_cli(int listenfd)
{
if(listenfd < 0)
{
puts("lisenfd < 0");
return -1;
}
int connfd = -1;
struct sockaddr_in mycli;
int len = sizeof(mycli);
connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
return connfd;
}
int do_work(int connfd)
{
if(connfd < 0)
{
puts("connfd < 0, cannot do work.");
return -1;
}
char buf[50];
memset(buf, 0, sizeof(buf));
int ret = recv(connfd, buf, sizeof(buf), 0);
if(ret > 0)
{
puts(buf);
send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
}
else if(ret == 0)
{
return -1;
}
return 0;
}
运行效果展示
结果分析
根据实验结果,可见后连接的也可以先进行通信,这就是高并发,展现出来的效果不同于阻塞的先后顺序处理。
2、多线程并发服务器
//伪代码
void *pthread_do_work(void *fd)
{
int connfd = (int)fd;
if(connfd < 0)
{
return NULL;
}
do_work(connfd);
return NULL;
}
socket
bind
listen
while(1)
{
connfd = accept()
pthread_create(&tid,NULL,pthread_do_work,(void *)connfd);
}
close listenfd
注意:
1、自定义的线程处理函数的后面不要有阻塞,否则会导致无法实现并发。
2、线程处理函数之后,不要写关闭通信套接字的语句,否则可能会导致,套接字已经被关闭,而无法被线程处理函数使用。
详细代码展示
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
int do_work(int connfd);
int accept_cli(int listenfd);
//子线程处理函数
void *pth_func_cli(void *fd)
{
int connfd = (int)fd;
if(connfd < 0)
{
return NULL;
}
printf("connfd:%d\n",connfd);
puts("do work");
do_work(connfd);
return NULL;
}
int main()
{
int fd = -1;
//创建一个套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
puts("socket fail");
return -1;
}
puts("socket success");
int ret = -1;
struct sockaddr_in myser; //现有的结构体类型是这个,后面需要进行一次强制的类型转换
myser.sin_family = AF_INET;
myser.sin_port = htons(7777);//设置端口号,5000以上可以随意设置,5000以下的被使用了
myser.sin_addr.s_addr = htonl(INADDR_ANY);
//给套接字绑定上IP和端口号,可以理解为使更新进化了一下这个套接字
ret = bind(fd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("bind fail");
close(fd);
fd = -1;
return -1;
}
puts("bind success");
//进行监听,最大可监听5个
ret = listen(fd, 5);
if(ret != 0)
{
puts("listen fail");
close(fd);
fd = -1;
return -1;
}
puts("listen start");
while(1)
{
//accept_cli
int connfd = accept_cli(fd);
if(connfd < 0)
{
continue;
}
else
{
puts("accept success.");
pthread_t tid;
int ret = pthread_create(&tid, NULL, pth_func_cli,(void *) connfd); //创建线程
if(ret != 0)
{
puts("pthread create error.");
close(connfd);
continue;
}
}
}
close(fd);
return 0;
}
//自定义函数部分
int accept_cli(int listenfd)
{
if(listenfd < 0)
{
puts("lisenfd < 0");
return -1;
}
int connfd = -1;
struct sockaddr_in mycli;
int len = sizeof(mycli);
connfd = accept(listenfd, (struct sockaddr *)&mycli, &len);
return connfd;
}
int do_work(int connfd)
{
if(connfd < 0)
{
puts("connfd < 0, cannot do work.");
return -1;
}
char buf[50];
memset(buf, 0, sizeof(buf));
int ret = recv(connfd, buf, sizeof(buf), 0);
if(ret > 0)
{
puts(buf);
send(connfd, buf, sizeof(buf), 0);//通过这里发回去给到发送方,这里的connfd是可以变的,也就是接收者可以改变
}
else if(ret == 0)
{
return -1;
}
return 0;
}
运行效果展示
多线程的服务器显现出来的效果上与多进程并无区别,区别在于多线程技术所用开销要比多进程技术小。
3、IO多路复用的并发服务器(利用select函数)
首先,Linux提供了三种IO多路复用的接口,分别是select、poll、epoll,在这里我们对select函数做详细展开。
select函数详解
函数形式
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
函数功能
实现对想要监听的文件描述符集合的监听,一旦有一个或者多个文件描述符进入IO就绪态时,select函数就会返回,执行相应的IO操作,否则,select函数会一直阻塞在那,并由内核(kernel)轮询地查看所有的文件描述符状态。
函数参数详解
参数nfds:最大fd+1,在三个描述符集合中找到最大的描述符+1
参数readfds:读文件描述符集合的首地址
参数writefds:写文件描述符集合的首地址
参数exceptfds:监听其他操作的文件描述符集合的首地址
参数timeout:一般给null,表示设置成阻塞,如果不想阻塞,可以设置struct timeval这个结构体的值,然后赋给timeout,表示要等待的秒数和微秒数
struct timeval {
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
返回值:成功,则返回所有准备好的文件描述符的个数,失败时,返回0表示超时,返回-1表示错误码。
select函数在对文件描述符进行操作时,主要用到以下这几个宏函数
宏函数 | 功能 |
FD_ZERO(fd_set *set) | 清空一个文件描述符集合 |
FD_CLR(int fd,fd_set *set) | 从文件描述符集合中清除一个文件描述符 |
FD_SET(int fd, fd_set *set) | 向文件描述符集合中添加一个文件描述符 |
FD_ISSET(int fd,fd_set *set) | 测试指定的文件描述符是否在该集合中 |
fd_set在后续程序中也有使用到,fd_set 本质上是个数组: long [32],是有系统给我们封装好的。此外,还定义了一个宏#define __FD_SETSIZE 1024,单进程中Linux最大的文件文件描述符监视数量为1024,这个值在内核中已经被定义,如需扩大,需重新编译内核。
实现原理
服务器基本框架
//伪代码
socket
bind
listen
准备读的文件描述符集合
清空读的文件描述符集合
将监听套接字加入读文件描述符集合中
while(1)
{
因为select会修改文件描述符集合,一旦修改,我们就不知道该监听谁了,因此,我们进入操作前要先备份
fd_set tempfds = readfds;
int fdnum = select(FD_SETSIZE, &tempfds, NULL, NULL, NULL);
if(fdunm > 0)
{
int i = 0;
for(i = 0;i < FD_SETSIZE;i++)
{
int connfd = accept();
if(connfd > 0)
{
将新接收到的通信套接字加入到文件描述符集合
FD_SET(connfd, &readfds);
}
else
{
continue;
}
}
else
{
do_work()
if客户端已关闭,recv的返回值为0时
{
FD_CLR(i, &readfds);
close(i);
}
}
}
}
close(listenfd)
详细代码展示(带详细注释)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
int main()
{
//socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0)
{
puts("socket error.");
return -1;
}
puts("socket success.");
struct sockaddr_in myser;
int ret = -1;
myser.sin_family = AF_INET;
myser.sin_port = htons(7777);
myser.sin_addr.s_addr = htonl(INADDR_ANY);
//myser.sin_addr.s_addr = inet_addr("192.168.1.1");或者像这样写也可以的
ret = bind(listenfd, (struct sockaddr *)&myser, sizeof(myser));
if(ret != 0)
{
puts("bind error.");
close(listenfd);
return -1;
}
puts("bind success.");
ret = listen(listenfd, 5);
if(ret != 0)
{
puts("listen error.");
close(listenfd);
return -1;
}
puts("listen success.");
//在此之前的都是服务器端初始化的基本必要操作
fd_set readfds,tempfds; //fd_set 本质上是个数组: long [32]
//此步创建了两个文件描述符集合,一个用来存放读文件描述符,一个用作临时变量
FD_ZERO(&readfds); //清空读文件描述符集合
FD_SET(listenfd, &readfds);//向读文件描述符集合中写入我们前边准备好的监听套接字
while(1)
{
tempfds = readfds; //赋值给另一个变量,自己保持不变
ret = select(FD_SETSIZE, &tempfds, NULL, NULL, NULL);
//select函数的调用,本次服务器搭建的关键
//FD_SETSIZE限制了文件描述符的最大值,最大值也就是个数
//&tempfds打开的是对读文件描述符集合的监听,其他几个都关闭
//最后一个参数给null表示设置select为阻塞
if(ret > 0) //ret大于0表示有文件描述符已经准备就绪,可以开始操作
{
int i = 0;
for(i = 0; i < FD_SETSIZE; i++) //遍历所有文件描述符,也就是所有文件来一遍
{
if(FD_ISSET(i, &tempfds))//判断,只对在读文件描述符集合中的文件进行进一步操作
{
if(i == listenfd) //如果是监听套接字
{
//accept
int connfd = -1;
struct sockaddr_in mycli;
int len = sizeof(mycli);
connfd = accept(i, (struct sockaddr *)&mycli, &len);//connfd返回的是通信套接字
if(connfd > 0) //如果服务器受理成功
{
FD_SET(connfd, &readfds); //将客户端的通信套接字,放入读文件描述符集合
}
else
{
continue; //没成功就继续
}
}
else //如果不是监听套接字,那就是通信套接字
{
//send or recv
char buf[50] = {0};
memset(buf, 0, 50);
ret = recv(i, buf, 50, 0); //从通信套接字中获取数据,并放入buf存储
if(ret > 0) //读取成功则输出
{
puts(buf);
send(i, buf, 50, 0); //并将数据再发回客户端
}
else if(ret == 0) //recv的返回值是0表示,客户端已经关闭
{
FD_CLR(i, &readfds); //将此客户端的通信套接字从读文件描述符集合中移除
close(i);//关闭这个文件
}
}
}
}
}
}
close(listenfd);
return 0;
}
运行结果展示