Linux Socket使用多线程实现两个客户端之间的echo(阿里云服务端转发数据,1个服务器,2个客户端)

307 阅读4分钟

一、任务描述

客户端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++编写的库。

总体实现思路如图所示:

image.png

二、服务端实现

总体思路:将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即可)

进入云服务器实例,找到安全组

image.png

点击进入一个安全组

image.png

添加一个入站规则,开放8888端口即可

image.png

至此完成云服务器的配置

5.2 运行echo_server.c

使用文件传输工具将c文件上传到服务器的 Linux /home/C文件夹中

image.png

使用Xshell工具连接到服务器,并进入到/home/C文件夹中

image.png

执行 gcc echo_server.c -o echo_server -lpthread

即可生成可执行文件echo_server, 然后执行命令./echo_server,运行即可

image.png

5.3 执行A_echo_client.c

在任意Linux终端打开c文件所在位置,这里我使用的是虚拟机。 执行gcc A_echo_client.c -o A_echo_client -lpthread

执行./A_echo_client,即可运行

image.png

5.4 执行B_echo_client.c

在任意Linux终端打开c文件所在位置,这里我也是使用虚拟机。 执行gcc B_echo_client.c -o B_echo_client -lpthread

执行./B_echo_client,即可运行

image.png

5.4 测试

根据连接进服务器的先后顺序,可以确定客户端0为A,客户端1为B,所以我们使用A向B发送消息。等待B回响数据。结果如下:

image.png

可见服务端转发了两次数据,一次是A向B发送,第二次是回响数据,B给A发送(因为B收到的信息包含了A的索引,所以只需要在服务端进行解析,就可以服务端就可以直接发送回去给A),A客户端从发送信息到接收信息的整个过程就是总耗时

至此,整个项目完成。