其他关联文章@丶4ut15m:
多线程并发服务器
多线程并发服务器和多进程兵法服务器的区别也不大,重点同样在于交互的处理
服务器
->创建套接字
->绑定地址结构
->监听套接字
->接收连接请求
->创建线程.pthread_create
->转存已连接客户端及套接字信息
->调用响应函数
->关闭套接字
首先需要自定义一个结构体用来存放客户端的信息(已连接套接字,地址结构等),而后调用响应函数与客户端进行交互.
子线程可以显式终止(调用pthread_exit函数),也可以隐式终止(不调用pthread_exit函数,子线程执行完响应函数后会自动终止)
关于线程安全.
使用pthread_key_create创建线程私有空间(TSD),这个函数的第二个参数为可选,是析构函数.如果设置了析构函数,那么在线程终止的时候会自动调用该函数.
线程有入口函数,也即是创建线程时线程自动执行的函数;析构函数则是线程终止之时调用的函数.
细节地方见作业四和代码.
服务器:
服务器等待接收客户的连接请求,连接成功则显示客户地址;
接收客户端的名称并显示;
然后接收来自该客户的字符串,对接收的字符串按分组进行加密(分组长度为个人学号,密钥为个人序号,分组不够补0);
将加密后的字符串发送给客户端;
继续等待接收该客户的信息,直到客户关闭连接;
要求服务器具有同时处理多个客户请求的能力。
客户端:
客户首先与相应的服务器建立连接;
接着接收用户输入的客户端名称,并将其发送给服务器;
接收用户输入的字符串并发送给服务器;
接收服务器发回的加密后的字符串并显示。
继续等待用户输入字符串;
若用户输入的是quit,则关闭连接并退出。
使用多线程实现
服务器代码
#include <stdio.h> //基本输入输出
#include <arpa/inet.h> //地址结构体所在头文件
#include <sys/socket.h> //套接字函数所在头文件
#include <string.h> //字符串处理函数所在头文件
#include <stdlib.h> //exit所在头文件
#include <pthread.h> //线程函数所在头文件
#include <unistd.h> //close所在头文件
#define PORT 3333
//自定义一个ARG结构体用以存放已连接套接字和客户端地址结构
struct ARG{
int connectfd;
struct sockaddr_in client;
};
//声明encode函数
void encode(int connectfd,struct sockaddr_in client);
//声明response函数
void response(int connectfd);
//声明function函数
void *function(void *arg);
int main(){
int listenfd,connectfd; //监听套接字和已连接套接字
struct sockaddr_in server,client; //地址结构
socklen_t len;
struct ARG *arg; //ARG结构体
pthread_t tid; //线程id
if((listenfd = socket(AF_INET,SOCK_STREAM,0))==-1){
perror("Socket error!\n");
exit(0);
}
//设置地址重用
int opt;
opt = SO_REUSEADDR;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//配置地址结构,用于给套接字绑定地址结构
memset(&server,'\0',sizeof(server)); //清空服务器地址结构
server.sin_addr.s_addr = htonl(INADDR_ANY); //设置监听地址为0.0.0.0也即是任意地址
server.sin_port = htons(PORT); //设置端口
server.sin_family = AF_INET; //设置协议族为IPv4
//绑定地址结构
if(bind(listenfd,(struct sockaddr *)&server,sizeof(server))==-1){
perror("Bind error!\n");
exit(0);
}
if(listen(listenfd,5) ==-1){
perror("Listen error!\n");
exit(0);
}
len = sizeof(client);
while(1){
if((connectfd = accept(listenfd,(struct sockaddr *)&client,&len)) ==-1){
perror("Accept error!\n");
exit(0);
}
//malloc一段空间将至类型转换为ARG* 类型,使得arg指向该段空间
arg = (struct ARG *)malloc(sizeof(struct ARG));
//存储已连接套接字和客户端地址结构
arg->connectfd = connectfd;
memcpy((void *)&arg->client,&client,sizeof(client));
/*创建线程
*函数原型
*pthread_create((pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *),void *arg)
*第一个参数为存放线程id的变量,第二个参数为线程属性默认为NULL,第三个参数为线程入口函数,第四个参数为入口函数的参数,如果不需要可以写NULL
*/
pthread_create(&tid,NULL,function,(void *)arg);
}
//关闭监听套接字
close(listenfd);
return 0;
}
void *function(void *arg){
//定义线程入口函数function
struct ARG *info; //新建一ARG指针类型结构体info用以指向上方arg指向的内存空间
/*每次获得连接arg都会新malloc一段区域,在线程入口函数里使得info指向该段内存空间
*便可以使得每个线程存放的已连接套接字不会丢失。
*若是不如此做,直接用arg来存放的话,每次新建线程arg里面的值都会便,会造成
*前面存放的数据(已连接套接字和客户端地址结构)丢失
*/
info = (struct ARG *)arg;
//调用response函数,用以获取并显示客户端名称
response(info->connectfd);
//调用encode函数用以将客户输入的数据进行加密并且发送给客户密文
encode(info->connectfd,info->client);
free(arg);
pthread_exit(NULL); //显式终止线程,如果不加该函数,在function完毕之后线程仍旧会终止,这便是隐式终止。
}
void response(int connectfd){
//获取用户输入的客户端名称
int num;
char client_name[20]; //存放客户端名称
memset(&client_name,'\0',sizeof(client_name)); //清空client_name变量
if((num = recv(connectfd,client_name,20,0) == -1)){
//接收客户端名字
perror("Recv error!\n");
exit(0);
}
client_name[num-1] ='\0'; //将最后一个字符置为空字符,用以作为字符串结尾
printf("Client name is %s\n",client_name);
}
//根据学号进行字符串加密
void encode(int connectfd,struct sockaddr_in client){
int i,j,num;
int snum[] = {2,0,1,7,1,2,2,1,1,9}; //密钥
char data[100],miwen[100]; //data用以接收用户输入的数据,miwen用以存放密文
j = 0;
while(1){
memset(&data,'\0',sizeof(data));
memset(&miwen,'\0',sizeof(miwen));
if((num = recv(connectfd,data,sizeof(data),0)) ==-1){
perror("Recv error!\n");
exit(0);
}
if(!strcmp(data,"quit")){
//如果用户输入的是quit则退出
break;
}
//密钥长度为学号长度,能整除该长度就说明不需要在字符串后面补零可以直接加密
for(i = 0; i<strlen(data); i++){
if((data[i] >= 'a' && data[i] <='z') || (data[i] >= 'A' && data[i] <='Z')){
//字符加密与数字加密需要分开,因为字符移位可能会越过字母的区域,需要另做操作
if(((data[i]+snum[j]) > 'z') && (data[i] >= 'a') && (data[i] <= 'z')){
//越界情况一
miwen[i] = data[i] + snum[j]-26;
}
else if(((data[i]+snum[j])>'Z') && (data[i] >= 'A') && (data[i] <= 'Z')){
//越界情况二
miwen[i] = data[i] + snum[j] -26;
}
else{
//这是正常情况执行的代码
miwen[i] = data[i] + snum[j];
}
//每次加密结束,j增加移至下一个密钥
j++;
if(j %10 == 0){
//如果j%10等于0,就说明已经加密完一轮,该让密钥回到第一个值的位置了
j = 0;
}
}
}
if(strlen(data) % 10 != 0){
//如果不能整除则需要填补。
for(i=0;i<(10-strlen(data)%10);i++){
miwen[strlen(data)+i] += 48;
miwen[strlen(data)+i] += snum[j];
j++;
if(j %10 == 0){
//如果j%10等于0,就说明已经加密完一轮,该让密钥回到第一个值的位置了
j = 0;
}
}
}
send(connectfd,miwen,sizeof(miwen),0); //发送密文
}
//循环完毕,关闭已连接套接字
close(connectfd);
}
服务器:
接收客户的连接请求,连接成功则显示客户地址;
接着接收客户端的名称并显示;
然后接收来自该客户的字符串,对接收的字符串按分组进行加密(分组长度为个人学号,密钥为个人序号,分组不够补0);
将加密后的字符发回客户端;
继续等待接收该客户的信息,直到客户关闭连接;
服务器将每个连接的用户所发来的所有数据存储起来;
当连接终止后,服务器将显示客户的名字及相应的所有数据;
要求服务器具有同时处理多个客户请求的能力。
客户端:
客户首先与相应的服务器建立连接;
接着接收用户输入的客户端名称,并将其发送给服务器;
继续接收用户输入的字符并将字符串发送给服务器;
同时接收服务器发回的加密后的字符串并显示;
继续等待用户输入字符串,直到用户输入的是quit,则关闭连接并退出。
打印出程序运行结果,并结合程序进行分析。
客户端代码和前面博客(多进程)相同,不再贴出
服务器代码
#include <stdio.h> //标准输入输出
#include <sys/socket.h> //套接字函数所在头文件
#include <string.h> //字符串处理函数所在头文件(例如memset)
#include <arpa/inet.h> //地址结构体所在头文件
#include <stdlib.h> //exit所在头文件
#include <pthread.h> //线程函数所在头文件
#include <unistd.h> //close所在头文件
#define PORT 3333
/*程序在savedata函数中调用createkey_once函数(自定义的)
*createkey_once函数调用pthread_key_create函数创建tsd
*并设置析构函数destructor.该析构函数用来释放线程创建的tsd
*
*/
//定义结构体
struct ARG{
int connectfd;
};
struct ST_DATA{
int index;
};
//声明全局变量
pthread_key_t key;
pthread_once_t once = PTHREAD_ONCE_INIT; //确保savedata中的pthread_once只执行一次
//定义静态函数
static void destructor(void *ptr){
//线程终止时自动调用该函数,用以释放线程私有空间key所占用的空间
free(ptr);
}
static void creatkey_once(void){
/*创建线程私有空间
*函数原型
*pthread_key_create(pthread_key_t *key,void(* destructor)(void*value));
*第一个参数指向创建的关键字,第二个参数可选,为析构函数
*/
pthread_key_create(&key,destructor);
}
void *function(void *arg); //声明函数
void response(int connectfd);
void encode(char *data,int len,char *cli_data,int connectfd);
void savedata(char *recvbuf,int len,char *cli_data);
int main(){
int listenfd,connectfd; //套接字
struct sockaddr_in server,client; //地址结构
struct ARG *arg;
pthread_t tid; //存放线程id
socklen_t len;
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) ==-1){
//创建套接字
perror("Socket error!\n");
exit(0);
}
//设置地址重用
int opt = SO_REUSEADDR;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//配置地址结构
memset(&server,'\0',sizeof(server));
server.sin_port = htons(PORT); //设置端口
server.sin_addr.s_addr = htonl(INADDR_ANY); //设置监听地址
server.sin_family = AF_INET; //设置协议
//绑定地址结构
if(bind(listenfd,(struct sockaddr *)&server,sizeof(server)) ==-1){
perror("Bind error!\n");
exit(0);
}
//监听
if(listen(listenfd,5) ==-1){
perror("Listen error!\n");
exit(0);
}
len = sizeof(client);
while(1){
//接受用户连接请求
if((connectfd = accept(listenfd,(struct sockaddr *)&client,&len)) ==-1){
perror("Accept error!\n");
exit(0);
}
//配置arg结构体
arg = (struct ARG *)malloc(sizeof(struct ARG)); //让arg指向内存中的一段空间
arg->connectfd = connectfd; //赋值
pthread_create(&tid,NULL,function,(void *)arg); //创建新线程
}
close(listenfd); //关闭监听套接字
return 0;
}
void *function(void *arg){
//线程入口函数
//保存arg参数的值
struct ARG *info;
info = (struct ARG *)arg; //使info指向arg指向的区域
response(info->connectfd); //调用响应函数
//响应完毕,释放arg空间
free(arg);
//显式终止线程,也可以没有下面这个代码,没有就是隐性终止线程
//线程入口函数运行完毕自动终止线程便是隐性终止
//调用函数进行终止则是显式终止
pthread_exit(NULL);
}
void response(int connectfd){
//前者用来存放客户端名称,中者用来存放客户端发送的所有消息,后者存放当前接收信息
char cli_name[30],cli_data[1000],data[100];
int num;
//清空cli_name
memset(&cli_name,'\0',sizeof(cli_name));
//接收客户端名称
if((num=recv(connectfd,cli_name,30,0)) ==-1){
perror("Recv cli_name error!\n");
exit(0);
}
//输出已连接信息
printf("Client's name is %s\n",cli_name);
while(1){
//每次接收消息前清空data
memset(&data,'\0',sizeof(data));
//接收消息
if((num = recv(connectfd,data,sizeof(data),0)) ==-1){
perror("Recv messsage error!\n");
exit(0);
}
if(!strcmp(data,"quit") || !strcmp(data,"quit\n")){
//若用户发送的是quit则退出
break;
}
num = strlen(data);
//将用户发送的消息进行保存
savedata(data,num,cli_data);
//加密消息
encode(data,num,cli_data,connectfd);
}
//交互完毕打印客户端名称及其发送的所有数据,并关闭已连接套接字
printf("Client name is %s\n",cli_name);
printf("The data is: %s\n",cli_data);
close(connectfd);
}
void encode(char *data,int len,char *cli_data,int connectfd){
int i,j,num;
int snum[] = {2,0,1,7,1,2,2,1,1,9}; //密钥
char miwen[100]; //data用以接收用户输入的数据,miwen用以存放密文
j = 0;
//清空密文数组
memset(&miwen,'\0',sizeof(miwen));
//密钥长度为学号长度,能整除该长度就说明不需要在字符串后面补零可以直接加密
for(i = 0; i<strlen(data); i++){
if((data[i] >= 'a' && data[i] <='z') || (data[i] >= 'A' && data[i] <='Z')){
//字符加密与数字加密需要分开,因为字符移位可能会越过字母的区域,需要另做操作
if(((data[i]+snum[j]) > 'z') && (data[i] >= 'a') && (data[i] <= 'z')){
//越界情况一
miwen[i] = data[i] + snum[j]-26;
}
else if(((data[i]+snum[j])>'Z') && (data[i] >= 'A') && (data[i] <= 'Z')){
//越界情况二
miwen[i] = data[i] + snum[j] -26;
}
else{
//这是正常情况执行的代码
miwen[i] = data[i] + snum[j];
}
//每次加密结束,j增加移至下一个密钥
j++;
if(j %10 == 0){
//如果j%10等于0,就说明已经加密完一轮,该让密钥回到第一个值的位置了
j = 0;
}
}
}
if(strlen(data) % 10 != 0){
//如果不能整除则需要填补。
for(i=0;i<(10-strlen(data)%10);i++){
miwen[strlen(data)+i] += 48;
miwen[strlen(data)+i] += snum[j];
j++;
if(j %10 == 0){
//如果j%10等于0,就说明已经加密完一轮,该让密钥回到第一个值的位置了
j = 0;
}
}
}
send(connectfd,miwen,sizeof(miwen),0); //发送密文
}
void savedata(char *recvbuf,int len,char *cli_data){
//保存用户输入的所有信息
int i=0; ///每次存放数据的索引
struct ST_DATA *data;
pthread_once(&once,creatkey_once); //pthread_once函数与once参数确保creatkey_once函数只执行一次
if((data = (struct ST_DATA *)pthread_getspecific(key)) ==NULL){
//获取线程私有空间
//如果是初次创建则进行初始化
data = (struct ST_DATA *)malloc(sizeof(struct ST_DATA));
pthread_setspecific(key,data);
data->index = 0;
}
while(i<len){
cli_data[data->index++] = recvbuf[i];
i++;
}
cli_data[data->index] = '\0';
}