>>了解文件描述符
文件描述符分为两类,一类是用于监听的,一类是用于通信的,在服务器端既有监听的,又有通信的。而且在服务器端只有一个用于监听的文件描述符,用于通信的文件描述符是有n个。和多少个客户端建立了连接,在服务器端就有多少个用于通信的文件描述符。在客户端只有一类文件描述符,就是用于通信的文件描述符。有了这些文件描述符,我们就可以进行网络io操作了。
网络io其实就是网络数据得到读或写操作。那么这个读或写操作的主体是谁呢?其实是内核里边的一块内存。在进行文件IO操作的时候,操作的是磁盘上的某一块内存。我们通过文件描述符就可以把数据从磁盘里边读出来,或者说把数据写入到磁盘中。我们在进行套接字通信的时候,每一个文件描述符它对应的是内核里边的两块内存,一块内存我们称之为读缓冲区,另一块内存我们称之为写缓冲区。读缓冲区是用来接收数据的,写缓冲区是用来发送数据的。
>>总结:在进行套接字通信的时候,每一个文件描述符都在内核的内存里边对应两块内存,一块内存我们称之为读缓冲区,用来接收数据的;另一块内存我们称之为写缓冲区,用来发送数据的。
>>pthread_create
基于linux下的高并发服务器开发(第三章)-(3.1-3.2)线程概述和创建_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/131878249?spm=1001.2014.3001.5501
一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程
称之为子线程。
程序中默认只有一个进程,fork()函数调用,2进行
程序中默认只有一个线程,pthread_create()函数调用,2个线程。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
成功:0
失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);
>>pthread_detach
基于linux下的高并发服务器开发(第三章)- 3.5 线程的分离_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/131880285?spm=1001.2014.3001.5501
/*
#include <pthread.h>
int pthread_detach(pthread_t thread);
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
1.不能多次分离,会产生不可预料的行为。
2.不能去连接一个已经分离的线程,会报错。
- 参数:需要分离的线程的ID
- 返回值:
成功:0
失败:返回错误号
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void * callback(void * arg) {
printf("chid thread id : %ld\n", pthread_self());
return NULL;
}
int main() {
// 创建一个子线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, callback, NULL);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error1 : %s\n", errstr);
}
// 输出主线程和子线程的id
printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());
// 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放
ret = pthread_detach(tid);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error2 : %s\n", errstr);
}
// 设置分离后,对分离的子线程进行连接 pthread_join()
// ret = pthread_join(tid, NULL);
// if(ret != 0) {
// char * errstr = strerror(ret);
// printf("error3 : %s\n", errstr);
// }
pthread_exit(NULL);
return 0;
}
网友评论lecranek:老师这集关于`pthread_create()`这里讲得蛮好的,是逐步引入了为什么`sockInfos`需要这样定义,而不是敲好代码直接带过。设置成全局变量是避免在栈上变量被销毁
多线程实现并发服务器
以下思路和文字总结来自爱编程的大丙:服务器并发 | 爱编程的大丙 (subingwen.cn)
大丙老师的课程也讲得非常棒(๑•̀ㅂ•́)و✧
多线程中的线程有两大类:主线程(父线程)和子线程,他们分别要在服务器端处理监听和通信流程。根据多进程的处理思路,就可以这样设计了:
- 主线程:
- 负责监听,处理客户端的连接请求,循环调用
accept()
函数 - 创建子线程:建立一个新的连接,就创建一个新的子线程,让这个子线程和对应的客户端通信
- 回收子线程资源:由于回收需要调用阻塞函数,这样就会影响
accept()
,直接做线程分离即可。
- 负责监听,处理客户端的连接请求,循环调用
- 子线程:负责通信,基于主线程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
- 发送数据:
send() / write()
- 接收数据:
recv() / read()
- 发送数据:
在多线程版的服务器端程序中,多个线程共用同一个地址空间,有些数据是共享的,有些数据的独占的,下面来分析一些其中的一些细节:
- 同一地址空间中的多个线程的栈空间是独占的
- 多个线程共享全局数据区,堆区,以及内核区的文件描述符等资源,因此
需要注意数据覆盖问题
,并且在多个线程访问共享资源的时候,还需要进行线程同步。
思路:首先accept是有一个线程的,另外只要这个accept成功的和一个客户端建立了连接,那么我们就需要创建一个对应的线程,用这个线程和客户端进行网络通信。每建立一个连接,通信的线程就需要创建出来一个。这样的话,能够保证通信的线程和客户端是一个一一对应的关系,也就是说用于通信的线程一共是有n个,用于建立连接的线程只有一个。在线程里边一共分为两类,一类是主线程,一类是子线程,只要是建立了新连接,主线程创建一个子线程,让子线程和对应建立连接的那个客户端去通信就行了。
这个图的思路和分析:我们需要在主线程里面不停的进行accept操作,如果说有新的客户端连接就建立连接。如果说没有新的客户端连接,主线程就阻塞在accept这个函数上。在主线程里边每创建一个新连接,就需要调用pthread_create创建一个子线程让这个子线程和对应的那个客户端进行网络通信。
考虑细节:多线程之间有哪些资源是共享的?哪些资源是不共享的?
全局和堆区是共享的,他们可以共同访问全局数据区里面的某一块内存或者说堆区里边的某一块内存。如果说有三个线程,那么这个栈区会被分成三份,每个线程都有一块属于自己的独立的栈空间,因此对于多个线程来说,他们并不是共享的。
1.sockInfo结构
struct sockInfo {
int fd;// 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid;// 线程号
};
struct sockInfo sockInfos[128];
需要注意父子线程共用同一个地址空间中的文件描述符,因此每当在主线程中建立一个新的连接,都需要将得到文件描述符值保存起来,不能在同一变量上进行覆盖,这样做丢失了之前的文件描述符值也就不知道怎么和客户端通信了。
在上面示例代码中是将成功建立连接之后得到的用于通信的文件描述符值保存到了一个全局数组中,每个子线程需要和不同的客户端通信,需要的文件描述符值也就不一样,只要保证存储每个有效文件描述符值的变量对应不同的内存地址,在使用的时候就不会发生数据覆盖的现象,造成通信数据的混乱了。
把结构体数组里边的每一个元素中的文件描述符设置为-1,这样的话,可以通过这个服务器来判断当前的数组元素是不是被占用的。如果这个数组元素被占用了,它的文件描述符的值应该是一个有效值。如果是-1,是无效值。也就意味着这个元素是空闲的,是可用的
2.初始化数据
// 初始化数据
int max = sizeof(sockInfos) / sizeof(sockInfos[0]);
for(int i = 0;i < max; i++) {
bzero(&sockInfos[i],sizeof(sockInfos[i]));
sockInfos[i].fd = -1;
sockInfos[i].tid = -1;
}
3.创建子线程
struct sockInfo* pinfo;
for(int i = 0;i < max;i++) {
// 从这个数组中找到一个可用的sockInfo元素
if(sockInfos[i].fd == -1) {
pinfo = &sockInfos[i];
break;
}
if(i == max - 1) {
sleep(1);
//i--;
i=-1;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr,&clientaddr,len);
// 创建子线程
pthread_create(&pinfo->tid,NULL,working,pinfo);
4.线程分离
pthread_detach(pinfo->tid);
server_thread.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct sockInfo {
int fd;// 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid;// 线程号
};
struct sockInfo sockInfos[128];
void* working(void* arg){
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo*)arg;
char clientIP[16];
inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,clientIP,sizeof(clientIP));
unsigned short clientPort = ntohs(9999);
printf("client ip is : %s, port is %d\n",clientIP,clientPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd,&recvBuf,sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n",recvBuf);
}else if(len == 0) {
printf("client closed...\n");
break;
}
write(pinfo->fd,recvBuf,strlen(recvBuf) + 1);
}
close(pinfo->fd);
pinfo->fd=-1;
return NULL;
}
int main() {
// 创建socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd,128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化数据
int max = sizeof(sockInfos) / sizeof(sockInfos[0]);
for(int i = 0;i < max; i++) {
bzero(&sockInfos[i],sizeof(sockInfos[i]));
sockInfos[i].fd = -1;
sockInfos[i].tid = -1;
}
// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while (1)
{
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
// 接受连接
int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len);
struct sockInfo* pinfo;
for(int i = 0;i < max;i++) {
// 从这个数组中找到一个可用的sockInfo元素
if(sockInfos[i].fd == -1) {
pinfo = &sockInfos[i];
break;
}
if(i == max - 1) {
sleep(1);
//i--;
i=-1;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr,&clientaddr,len);
// 创建子线程
pthread_create(&pinfo->tid,NULL,working,pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
client.c
// TCP 通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.88.129",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
int i = 0;
while(1) {
sprintf(recvBuf,"data : %d\n",i++);
// 给服务端发送数据
write(fd,recvBuf,strlen(recvBuf)+1);
int len = read(fd,recvBuf,sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv server : %s\n",recvBuf);
}else if(len == 0) {
//表示服务器断开连接
printf("server closed....\n");
break;
}
sleep(1);
}
return 0;
}
相关socket的api可以看这一篇,有介绍到:
基于linux下的高并发服务器开发(第四章)- 多进程实现并发服务器(回射服务器)_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132021287?spm=1001.2014.3001.5501