一、任务描述
客户端A,B均运行Ubuntu。 云端服务器C,运行Ubuntu。
A,B各自与C建立TCP连接。
A发送TCP包给C,C转发至B。 B收到后原封发回C,C转发至A。 A将所用时间显示出来。
要求: A,B,C端全部程序使用C/C++编写,可使用外部库,但仅限于C/C++编写的库。
总体实现思路如图所示:
二、服务端实现
总体思路:将socket连接设置为非阻塞,使用一个结构体数组在server端维护每一个连接进来的客户端信息,对客户端发来的消息进行拆解(包含接收方索引以及主体消息),根据索引发送给指定客户端,实现消息转发的作用。
IP:服务器运行所在主机
端口:8888
echo_server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define PORT 8888
#define BACKLOG 10
#define MAXCONN 100
#define BUFFSIZE 1024
typedef unsigned char BYTE;
//客户端信息
typedef struct ClientInfo
{
struct sockaddr_in addr;
int clientfd;
int isConn;
int index;
} ClientInfo;
pthread_mutex_t activeConnMutex;
pthread_mutex_t clientsMutex[MAXCONN];
pthread_t threadID[MAXCONN];
ClientInfo clients[MAXCONN];
//转换为小写
void tolowerString(char *s)
{
int i=0;
while(i < strlen(s))
{
s[i] = tolower(s[i]);
++i;
}
}
//新建一个线程,管理各自的客户端
void clientManager(void* argv)
{
ClientInfo *client = (ClientInfo *)(argv);
BYTE buff[BUFFSIZE];
int recvbytes;
int i=0;
int clientfd = client->clientfd;
struct sockaddr_in addr = client->addr;
int isConn = client->isConn;
int clientIndex = client->index;
//接收数据
while((recvbytes = recv(clientfd, buff, BUFFSIZE, 0)) != -1)
{
tolowerString(buff); //格式转换
char msg[BUFFSIZE]; //客户端消息主体
int dest = clientIndex; //消息发送的目标客户端
sscanf(buff, "%d%s", &dest, msg);
fprintf(stdout, "发送端索引:%d 接收端索引:%d ,消息为:%s\n",clientIndex, dest, msg);
//加上对目标客户端互斥锁
pthread_mutex_lock(&clientsMutex[dest]);
//printf("发送端索引:%d 接收端索引:%d\n",clientIndex,dest);
char str[10];
sprintf(str,"%d ",clientIndex);
strcat(str,msg);
//给目标客户端发送信息
if(send(clients[dest].clientfd, str, strlen(str)+1, 0) == -1)
{
fprintf(stderr, "消息发送失败\n");
pthread_mutex_unlock(&clientsMutex[dest]);
break;
}
printf("消息发送成功\n");
//释放锁
pthread_mutex_unlock(&clientsMutex[dest]);
} //end while
}
int main()
{
int i=0;
for(;i<MAXCONN;++i)
//初始化客户端数组互斥锁
pthread_mutex_init(&clientsMutex[i], NULL);
for(i=0;i<MAXCONN;++i)
//初始化客户端连接
clients[i].isConn = 0;
int listenfd;
struct sockaddr_in servaddr;
//新建一个socket
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stdout, "创建socket失败\n");
exit(0);
}
else
fprintf(stdout, "创建socket成功\n");
fcntl(listenfd, F_SETFL, O_NONBLOCK); //设置socket不阻塞 F_SETFL:设置文件状态标记
//设置server地址
memset(&servaddr, 0, sizeof(servaddr)); //初始化服务地址
servaddr.sin_family = AF_INET; //AF_INET 使用TCP协议
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任何输入地址
servaddr.sin_port = htons(PORT); //设置端口
//绑定socket
if(bind(listenfd, (struct sockaddr*)(&servaddr), sizeof(servaddr)) == -1)
{
fprintf(stdout, "绑定socket失败\n");
exit(0);
}
else
fprintf(stdout, "绑定socket成功\n");
//listen状态
if(listen(listenfd, BACKLOG) == -1)
{
fprintf(stdout, "监听socket失败\n");
exit(0);
}
else
fprintf(stdout, "监听socket成功\n");
while(1)
{
//为新连接找一个空位置
int i=0;
while(i<MAXCONN)
{
//上锁
pthread_mutex_lock(&clientsMutex[i]);
if(!clients[i].isConn)
{
pthread_mutex_unlock(&clientsMutex[i]);
break;
}
//释放锁
pthread_mutex_unlock(&clientsMutex[i]);
++i;
}
//连接已满,初始化第一个连接
if (i == MAXCONN) i = 0;
//accept状态
struct sockaddr_in addr;
int clientfd;
int sin_size = sizeof(struct sockaddr_in);
if((clientfd = accept(listenfd, (struct sockaddr*)(&addr), &sin_size)) == -1)
{
sleep(1);
continue;
}
else
fprintf(stdout, "客户端%d连接成功\n",i);
//给当前客户端连接上锁
pthread_mutex_lock(&clientsMutex[i]);
//放入客户端数据
clients[i].clientfd = clientfd;
clients[i].addr = addr;
clients[i].isConn = 1;
clients[i].index = i;
//释放
pthread_mutex_unlock(&clientsMutex[i]);
//新建一个线程管理当前客户端
pthread_create(&threadID[i], NULL, (void *)clientManager, &clients[i]);
} //结束
}
三、客户端A实现
A_echo_client.c
IP以及端口填写服务器运行的IP以及port
总体思路:使用新建线程接收数据,在主线程发送数据,在发送数据前记录时间,在接收数据的时候再次记录时间,前后的时间差即为echo所花费的时间。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>//计算时间
#define PORT 8888//端口
#define IP "120.79.165.96"//服务端IP地址
#define BUFFERSIZE 1024
typedef unsigned char BYTE;
pthread_t receiveID;
time_t c_start,c_end;//时间
//字符格式转换
void tolowerString(char *s)
{
int i=0;
while(i < strlen(s))
{
s[i] = tolower(s[i]);
++i;
}
}
//接收服务端发过来的数据
void receive(void *argv)
{
int sockclient = *(int*)(argv);
BYTE recvbuff[BUFFERSIZE];
while(recv(sockclient, recvbuff, sizeof(recvbuff), 0)!=-1) //receive
{
BYTE msg[BUFFERSIZE];
int dest = 0;//存储的是在服务端发送方的id,在这里不需要使用
sscanf(recvbuff, "%d%s",&dest,msg);
c_end = clock();//接收到数据
fprintf(stdout, "\n接收到信息\n发送端索引:%d 消息为:%s\n", dest, msg);
fprintf(stdout, "耗时 %.2f ms \n\n",difftime(c_end,c_start));
}
}
int main()
{
//新建socket
int sockclient = socket(AF_INET,SOCK_STREAM, 0);
//地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
//设置初始值
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT); ///server port
servaddr.sin_addr.s_addr = inet_addr(IP); //server ip
//连接服务端
if (connect(sockclient, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
fprintf(stderr,"连接服务器失败");
exit(0);
}
fprintf(stdout, "连接服务器成功\n");
//添加一个接收数据的线程
pthread_create(&receiveID, NULL, (void *)(receive), (void *)(&sockclient));
BYTE buff[BUFFERSIZE];
fprintf(stdout, "输入格式:(客户端索引) (消息) 例如 0 hello!ps:索引是按客户端启动顺序,从0开始\n");
while (fgets(buff, sizeof(buff), stdin) != NULL)
{
tolowerString(buff);
//主线程发送数据
c_start = clock();//开始时间
if(send(sockclient, buff, strlen(buff)+1, 0) == -1) //send
{
fprintf(stderr, "发送失败\n");
continue;
}
//清空
memset(buff, 0, sizeof(buff));
}
close(sockclient);
return 0;
}
四、回响客户端B实现
B_echo_client.c
IP以及端口填写服务器运行的IP以及port
总体思路:新建一个接收数据的线程,主线程发送数据(直接复制了客户端A的代码,可取消这个功能),至于回响操作,可以直接将服务端发送过来的数据,直接原封不动的发回去。(数据中已经包含发送方,也就是客户端A的id,服务端可以根据这个id直接将数据返回给客户端A)
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>//计算时间
#define PORT 8888//端口
#define IP "120.79.165.96"//服务端IP地址
#define BUFFERSIZE 1024
typedef unsigned char BYTE;
pthread_t receiveID;
//字符格式转换
void tolowerString(char *s)
{
int i=0;
while(i < strlen(s))
{
s[i] = tolower(s[i]);
++i;
}
}
//接收服务端发过来的数据
void receive(void *argv)
{
int sockclient = *(int*)(argv);
BYTE recvbuff[BUFFERSIZE];
while(recv(sockclient, recvbuff, sizeof(recvbuff), 0)!=-1) //receive
{
BYTE msg[BUFFERSIZE];
int dest = 0;//存储的是在服务端发送方的id,在这里不需要使用
sscanf(recvbuff, "%d%s",&dest,msg);
fprintf(stdout, "接收到信息\n发送端索引:%d 消息为:%s\n", dest, msg);
fprintf(stdout, "消息回响\n\n");
//返回回响数据
send(sockclient, recvbuff, strlen(recvbuff)+1, 0);
}
}
int main()
{
//新建socket
int sockclient = socket(AF_INET,SOCK_STREAM, 0);
//地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
//设置初始值
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT); ///server port
servaddr.sin_addr.s_addr = inet_addr(IP); //server ip
//连接服务端
if (connect(sockclient, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
fprintf(stderr,"连接服务器失败");
}
fprintf(stdout, "连接服务器成功\n");
//添加一个接收数据的线程
pthread_create(&receiveID, NULL, (void *)(receive), (void *)(&sockclient));
BYTE buff[BUFFERSIZE];
while (fgets(buff, sizeof(buff), stdin) != NULL)
{
tolowerString(buff);
//主线程发送数据
if(send(sockclient, buff, strlen(buff)+1, 0) == -1) //send
{
fprintf(stderr, "发送失败\n");
continue;
}
memset(buff, 0, sizeof(buff));
}
close(sockclient);
return 0;
}
五、操作步骤
5.1 服务器配置
开放阿里云服务器端口8888,(为了测试也可以跳过这个步骤,直接在本机运行,客户端的IP修改为127.0.0.1即可)
进入云服务器实例,找到安全组
点击进入一个安全组
添加一个入站规则,开放8888端口即可
至此完成云服务器的配置
5.2 运行echo_server.c
使用文件传输工具将c文件上传到服务器的 Linux /home/C文件夹中
使用Xshell工具连接到服务器,并进入到/home/C文件夹中
执行 gcc echo_server.c -o echo_server -lpthread
即可生成可执行文件echo_server,
然后执行命令./echo_server,运行即可
5.3 执行A_echo_client.c
在任意Linux终端打开c文件所在位置,这里我使用的是虚拟机。
执行gcc A_echo_client.c -o A_echo_client -lpthread
执行./A_echo_client,即可运行
5.4 执行B_echo_client.c
在任意Linux终端打开c文件所在位置,这里我也是使用虚拟机。
执行gcc B_echo_client.c -o B_echo_client -lpthread
执行./B_echo_client,即可运行
5.4 测试
根据连接进服务器的先后顺序,可以确定客户端0为A,客户端1为B,所以我们使用A向B发送消息。等待B回响数据。结果如下:
可见服务端转发了两次数据,一次是A向B发送,第二次是回响数据,B给A发送(因为B收到的信息包含了A的索引,所以只需要在服务端进行解析,就可以服务端就可以直接发送回去给A),A客户端从发送信息到接收信息的整个过程就是总耗时
至此,整个项目完成。