网络的重要性
两个主机之间通信离不开网络,分布式,对数据库的操作,fpc,消息队列的组件,都离不开网络。网络主要是解决机器与机器之间通信的问题。网络有很多种方式,比如tcp、udp、广播等,网络中最重要的协议是tcp。
单线程tcp
酒店(服务器)有很多门(端口),酒店门口有迎宾员(listenfd),酒店中有服务员(clientfd),酒店回来客人(客户端),迎宾员将客人送进酒店介绍给服务员(accept),后面一系列的服务就都由服务员来服务。有n个客户就有n个服务员。
下面是一段简单的tcp回声服务器代码:
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//指派迎宾员去哪个门口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开始迎宾
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//接待客人
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd/*产生服务员*/ = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
close(listenfd);
return 0;
}
用网络调试助手去连接改服务器的时候,可以多次连接,但是只能有一个连接发送数据,服务端可以接收到呢?
首先我们要知道,在listen的时候,客户端点击连接后,就会发生tcp的三次握手,而tcp的三次握手是在内核中的协议栈中发生的,与应用层没有关系,三次握手并不发生在任何一个api中,它是协议栈本身完成的,处于listen状态时,被动完成的。进入listen状态之后,三次握手是允许的,但是,应用层并没有把这个连接拿出来去使用。accept之后会取一个连接出来使用。
将代码改成可以接收多个客户端数据
接下来要将代码改成可以有多个客户端同时发消息的代码
想法1 将accept函数放入while循环
这样会使得可以连接多个客户端,但是每个客户端只能接收一条消息
accept拿一个连接节点–> recv阻塞 -->接收数据后send–>用同一个connfd进行下一个接收
想法2 多线程
头文件:#include <pthread.h>
线程创建函数:pthread_create
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define MAXLNE 4096
void *client_routine(void *arg)
{
int connfd = *(int *)arg;
char buff[MAXLNE];
while (1)
{
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
break;
}
}
return NULL;
}
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//指派迎宾员去哪个门口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开始迎宾
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd /*产生服务员*/ = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
}
close(listenfd);
return 0;
}
这种服务器适合4,5个连接的场景,如会议室,但是有大量的客户端就不行了。按照posix线程标准8m来算,1G的内存,也只能连128个。内存涨到一个瓶颈的时候,系统就会重启。很难突破C10K的数量级
- 优点:逻辑简单
- 缺点:不适合大量的客户端
想法3 io多路复用组件select
当大量的客户端连进来的时候,我们并不知道要处理哪个客户端,这就需要一个组件,某个客户端发数据的时候,让我们立马可以知道这个客户端发信息来了。我们需要做的事情是,把众多的fd,放在一个组件中集中处理
相当于将一个服务员服务一个客人 变成了 让一个服务员集中处理多桌客人
fd_set rfds; //fd的集合,是一个bitset
系统提供了4个宏对描述符集进行操作:
- 宏FD_SET设置文件描述符集fdset中对应于文件描述符fd的位(设置为1)
- 宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)
- 宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。
使用这3个宏在调用select前设置描述符屏蔽位 - 在调用select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。
select
参考博客:https://blog.youkuaiyun.com/lingfengtengfei/article/details/12392449
int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
参数1:最大的fd, select中会有一个循环的操作,遍历bitmap,第一个参数就是i<n的n,所以第一个参数就是填FD_SET设置的fd中最大的fd加上1(fd是从0开始的)
select函数内部有一个置0的操作,所以在**if (FD_ISSET(i, &rset))**内要用 FD_SET宏重新置1
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//指派迎宾员去哪个门口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开始迎宾
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// select
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds); //监控listenfd
int max_fd = listenfd;
while (1)
{
rset = rfds;
int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset))
{ //
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds); //将新产生的连接fd加入fd_set
if (connfd > max_fd)
max_fd = connfd;
if (--nready == 0)
continue;
}
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{ //listenfd后面就是connfd,依次对connfd进行读写操作
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
close(i);
}
if(--nready == 0) break;
}
}
}
close(listenfd);
return 0;
}
标准send写法:
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//指派迎宾员去哪个门口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开始迎宾
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// select
fd_set rfds, rset, wfds, wset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds); //监控listenfd
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1)
{
rset = rfds;
wset = wfds;
int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
if (FD_ISSET(listenfd, &rset))
{ //
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds); //将新产生的连接fd加入fd_set
if (connfd > max_fd)
max_fd = connfd;
if (--nready == 0)
continue;
}
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{ //listenfd后面就是connfd,依次对connfd进行读写操作
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
FD_SET(i, &wfds);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
printf("disconnect\n");
close(i);
}
if(--nready == 0) break;
} else if(FD_ISSET(i, &wset)) {
send(i, buff, n, 0);
FD_CLR(i, &wfds);
FD_SET(i, &rfds);
}
}
}
close(listenfd);
return 0;
}
一个select可以支持sizeof(fd_set) * 8个fd, 多开几个select进程就可以突破C10k数量级的限制,但是突破不了C1000k数量级的限制,因为调用select的时候,会把要监控的集合copy到内核中,多个客户端可能只有几个可以操作,然后再copy出来。