先导知识:
在Linux系统实现服务器端和客户端的套接字通信_小梁今天敲代码了吗的博客-CSDN博客
线程同步(一)_小梁今天敲代码了吗的博客-CSDN博客
线程同步(二)_小梁今天敲代码了吗的博客-CSDN博客
线程同步(三)_小梁今天敲代码了吗的博客-CSDN博客
如果要编写多进程版的并发服务器程序,首先要考虑,创建出的多个进程都是什么角色,这样就可以在程序中对号入座了。在Tcp服务器端一共有两个角色,分别是:监听和通信,监听是一个持续的动作,如果有新连接就建立连接,如果没有新连接就阻塞。关于通信是需要和多个客户端同时进行的,因此需要多个进程,这样才能达到互不影响的效果。进程也有两大类:父进程和子进程,通过分析我们可以这样分配进程:
- 父进程:
- 负责监听,处理客户端的连接请求,也就是在父进程中循环调用
accept()
函数 - 创建子进程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
- 回收子进程资源:子进程退出回收其内核PCB资源,防止出现僵尸进程
- 负责监听,处理客户端的连接请求,也就是在父进程中循环调用
- 子进程:负责通信,基于父进程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
- 发送数据:
send() / write()
- 接收数据:
recv() / read()
- 发送数据:
在多进程版的服务器端程序中,多个进程是有血缘关系,对应有血缘关系的进程来说,还需要想明白他们有哪些资源是可以被继承的,哪些资源是独占的,以及一些其他细节:
-
子进程是父进程的拷贝,在子进程的内核区PCB中,文件描述符也是可以被拷贝的,因此在父进程可以使用的文件描述符在子进程中也有一份,并且可以使用它们做和父进程一样的事情。
-
父子进程有用各自的独立的虚拟地址空间,因此所有的资源都是独占的
-
为了节省系统资源,对于只有在父进程才能用到的资源,可以在子进程中将其释放掉,父进程亦如此。
-
由于需要在父进程中做
accept()
操作,并且要释放子进程资源,如果想要更高效一下可以使用信号的方式处理
服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//提供IP地址转换函数
#include <pthread.h>
//信息结构体
struct SockInfo
{
struct sockaddr_in addr;
int fd;
};
struct SockInfo infos[512];//用来存储客户端信息
//声明子线程任务函数
void* working(void* arg);
int main()
{
//1.创建监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");//perror(s) 用来将上一个函数发生错误的原因输出到标准设备,参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
return -1;
}
//2.绑定本地的IP port
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//地址族协议
saddr.sin_port = htons(9999);//端口必须转换成大端
saddr.sin_addr.s_addr = INADDR_ANY;//指定IP地址(INADDR_ANY泛指本机的意思,也就是表示本机的所有IP)
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)//绑定失败 返回-1
{
perror("bind");
return -1;
}
//3设置监听
ret = listen(fd, 128);
if (ret == -1)
{
perror("listen");
return -1;
}
//初始化结构体数组
int max = sizeof(infos) / sizeof(infos[0]);
for (int i = 0; i < max; ++i)
{
bzero(&infos[i], sizeof(infos[i]));
infos[i].fd = -1;
}
//4阻塞并等待客户端的连接
int addrlen = sizeof(struct sockaddr_in);
while (1)
{
struct SockInfo* pinfo;
for (int i = 0; i < max; ++i)
{
if (infos[i].fd == -1)
{
pinfo = &infos[i];
break;
}
}
int cfd = accept(fd, (struct sockaddr*)&pinfo->addr, &addrlen);
pinfo->fd = cfd;
if (cfd == -1)
{
perror("accept");
break;
}
//创建子线程
pthread_t tid;
pthread_create(&tid, NULL, working, pinfo);
pthread_detach(tid);
}
close(fd);
return 0;
}
void* working(void* arg)
{
struct SockInfo* pinfo = (struct SockInfo*)arg;//将传进来的arg进行类型转换
//连接建立成功,打印客户端的IP和端口信息
char ip[32];
printf("客户端的ip:%s,端口:%d\n",
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port));//网络字节序转换为主机字节序
//5.通信
while (1)
{
//接收数据
char buff[1024];
int len = recv(pinfo->fd, buff, sizeof(buff), 0);
if (len > 0)
{
printf("client say: %s\n", buff);
send(pinfo->fd, buff, len, 0);
}
else if (len == 0)
{
printf("客户端已经断开了连接...\n");
break;
}
else
{
perror("recv");
break;
}
}
//关闭文件描述符
close(pinfo->fd);
pinfo->fd = -1;
return NULL;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//提供IP地址转换函数
int main()
{
//1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1)
{
perror("socket");//perror(s) 用来将上一个函数发生错误的原因输出到标准设备,参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
return -1;
}
//2.连接服务器IP port
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//地址族协议
saddr.sin_port = htons(9999);//端口必须转换成大端
inet_pton(AF_INET, "192.168.122.1 ", &saddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret == -1)//连接失败 返回-1
{
perror("connect");
return -1;
}
int number = 0;
//3.通信
while (1)
{
//发送数据
char buff[1024];
sprintf(buff, "你好,hello ,world ,%d...\n", number++);
send(fd, buff, strlen(buff) + 1, 0);
//接收数据
memset(buff, 0, sizeof(buff));
int len = recv(fd, buff, sizeof(buff), 0);
if (len > 0)
{
printf("server say: %\n", buff);
}
else if (len == 0)
{
printf("服务器端已经断开了连接...\n");
break;
}
else
{
perror("recv");
break;
}
sleep(1);
}
//关闭文件描述符
close(fd);
return 0;
}
运行结果:
从图中可以看到server服务器端可以收到两个client客户端的通信信息,即多线程并发通信成功
在编写多线程版并发服务器代码的时候,需要注意父子线程共用同一个地址空间中的文件描述符,因此每当在主线程中建立一个新的连接,都需要将得到文件描述符值保存起来,不能在同一变量上进行覆盖,这样做丢失了之前的文件描述符值也就不知道怎么和客户端通信了。
在上面示例代码中是将成功建立连接之后得到的用于通信的文件描述符值保存到了一个全局数组中,每个子线程需要和不同的客户端通信,需要的文件描述符值也就不一样,只要保证存储每个有效文件描述符值的变量对应不同的内存地址,在使用的时候就不会发生数据覆盖的现象,造成通信数据的混乱了。