客服端\服务器端程序(实现BBS)

时间:2022-12-02 08:58:40

/*

 *Linux_C课程实践项目: (仅供学习参考)

    Linux环境命令行下 BBS 服务

    功能:

        a)  BBS服务器端提供分类的BBS信息,如:新闻、娱乐、供求、...

            BBS客户端登录到BBS服务器后,服务器首先提供一个简单的

            选择菜单供客户端选择信息类型,客户选中后,服务端将相应

            的类目信息提供给客户端

        b)  BBS客户发出的相应类目下的消息服务端要保存。

            服务程序可以将每种类目数据保存成单个独立文件,有几种信息类型,就保存几个文件

        c)  BBS服务端既要将历史数据在BBS客户登录时发给客户端,而且已

            登入的BBS客户端当前发出的消息要及时的广播给已经登录的其他BBS客户(按类目)

 

    功能实现:新闻发部与浏览

              登陆服务端,服务端发送主菜单,等等用户选择 1 > 进入新闻模式

              新闻模式 1 > 看新闻 2 > 发新闻 0 > return

              选择 1 每次发一条新闻(一行信息) 用户键入回车查看下一条信息,键入q时退出

              选择 2 写入要发布的信息,直到写入return为止,然后通知其他浏览新闻信息的客户

 

    实现原理:(新闻)  

      运行服务程序,

        int serv_init(void) 创建套接字、绑定端口、监听网络端口,等待客户连接

        void initlist()  初始化链表头

        主线程进入无限循环

        clientfd = accept(); 等待接收链接请求

 

      连接成功

        打印客户端IP及端口号

        保存客户IP及端口信息至动态分配的结构体中,并加入链表 Inselem()

        pthread_create() 创建新线程进行通信,把保存IP及端口信息结构体传入线程函数

            send() 发送主菜单信息给客户

            选择 1 进入新闻模式

                   client->flag = 1;             标记客户新闻模式

            read() 等待客户选择 1 > 看新闻 2 > 发新闻 exit > 退出 等待客户输入命令:过虑单独的回车     

            客户选择 1

                client->flag += 10;              标记客户看新闻模式

                file_read() 读文件 看新闻

                    pthread_rwlock_rdlock(&rwlock); 上读锁

                    fgets()  一行(一条新闻)读文件

                    pthread_rwlock_unlock(&rwlock); 解锁

                    send()  发送新闻信息

                    recv()  等待客户选择下一条还是退出

                client->flag -= 10;              清除还原

 

            客户选择 2

                file_write() 写文件 发新闻

                    recv() 接收客户输入的信息

                    pthread_rwlock_wrlock() 上写锁

                    fputs(); 写入文件

                    pthread_rwlock_unlock() 解锁

                Locate() 查找正在浏览新闻的的有客户端

                    send();    提示客户端有新新闻

           

            客户选择 0 返回上级菜单

                返回主菜单

               

        客户选择 0  客户端退出 

            发送退出信息给客户端

            close() 关闭客户端

            Delelem() 在链表中删除客户端信息

        pthread_exit() 线程退出

 

   知识点:网络编程 多线程编程 线程间同步 链表操作 系统文件编程字符串操作

 

 */

 

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>

 

#define SERV_PORT 5555           //定义服务器端口号

#define BACKLOG       256             //请求队列的最大个数

#define BUF_SIZE  1024

 

 

char menu[] = "\n-----welecom bbs-----\n 1 > news\n 2 > recreation\n 0 > exit\n";               //主菜单

char menu_news[] = "\n-----welecom news-----\n 1 > read news\n 2 > write news\n 0 > return\n";    //新闻菜单 

char menu_recreation[] = "\n-----welecom recreation-----\n 1 > read recreation\n 2 > write recreation\n 0 > return\n"; //娱乐菜单

 

typedef struct LNode

{

    int clientfd;

    struct sockaddr_in client_addr;                 //客户端口号和IP

    int flag;                                      //用户标记

    struct LNode *next;

}LinkList;

LinkList *head;                                       //定义链表头节点为全局变量

 

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;       //静态初始化读写锁

 

 

/*

 功能:服务端程序初始化函数

 说明:创建套接字、绑定端口、监听网络端口

 返回:Sock文件描述符

 */

int serv_init(void)

{

    int ret;

    int sockfd;                               //定义Sock文件描述符  

    struct sockaddr_in   host_addr;         //定义本机端口号和IP  

 

    /*创建套接字*/

    sockfd = socket(AF_INET, SOCK_STREAM, 0);     //使用TIP\IP,数据流套接字

    if(sockfd < 0)                                    //出错检查

    {

        perror("socket");

        exit(0);

    }

 

    /*绑定套接字*/

    bzero(&host_addr, sizeof host_addr);           //结构体清0

    host_addr.sin_family = AF_INET;                 //TIP\IP

    host_addr.sin_port = htons(SERV_PORT);         //设定端口号

    host_addr.sin_addr.s_addr = INADDR_ANY;           //使用本机IP

    ret = bind(sockfd, (struct sockaddr *)&host_addr, sizeof host_addr);  //绑定端口

    if(ret < 0)

    {

        perror("bind");

        exit(0);

    }

 

    ret = listen(sockfd, BACKLOG);              //监听网络端口

    if(ret < 0)

    {

        perror("listen");

        exit(0);

    }

 

    return sockfd;

}

 

/*

 初始化链表头

 */

void initlist(LinkList **L)

{

    (*L)= (LinkList *)malloc(sizeof(LinkList));

    (*L)->next = NULL;

}

 

/*

 插入节点 在头节点后第一个节点之前插入节点

 */

void Inselem(LinkList *head, LinkList *s)

{

    s->next = head->next;            //新节点指针域指向下一个节点地址

    head->next = s;                   //头指针指向新节点地址

}

 

/*

 查找节点 按关键字flag

 */

LinkList *Locate(LinkList *L, int x)

{

    LinkList *= L->next;                       //从下一个节点开始查找

   

    while(!= NULL && p->flag != x)           //遍历链表

        p = p->next;

       

    return p;

}

 

/*

 删除节点  按客户端sock文件描述符clientfd

 */

void Delelem(LinkList *L, int x)

{

    LinkList *p1,                 //要删除节点前一个节点的位置

             *p2;              //要删除节点后一个节点的位置

             

    LinkList *= L;                      //从下一个节点开始查找

   

    while(!= NULL && p->clientfd != x)           //遍历链表

    {

        p1 = p;

        p = p->next;

    }

    if(== NULL)                         //没有找到相应的节点

        return ;

    else

    {

        p2 = p->next;

        p1->next = p2;

        free(p);                               //删除该节点

    }

}

 

/*

 读文件发送文件内容给客户端

 以只读方式打开文件,依次读取每行文件内容发送给客户端

 入口参数file->打开文件名,clientfd->客户端sock描述符

 */

void file_read(char *file, int clientfd)

{

    FILE *fp;                              //定义文件指针

    char buf[BUF_SIZE];

 

    fp = fopen(file, "r");                           //以只读方式打开文件

    if(fp == NULL)

    {

        printf("%s %d", __FUNCTION__, __LINE__);

        perror("fopen");

        return ;

    }

   

    /*sizeof要减一,用strlen就不要*/

    send(clientfd, "\n------read(q)------\n", sizeof("\n------read(q)------\n")-1, 0);              //发送提示信息

    bzero(buf, sizeof buf);

 

    while(!feof(fp))         

    {

        bzero(buf, sizeof buf);

        pthread_rwlock_rdlock(&rwlock);             //读写锁上读锁

        fgets(buf, BUF_SIZE , fp);                  //依次读取文件行

        pthread_rwlock_unlock(&rwlock);             //读写锁解锁

 

        if(send(clientfd, buf, strlen(buf), 0) < 0)   //发送信息内容

        {

            printf("%s %d", __FUNCTION__, __LINE__);

            perror("send");

        }

       

        bzero(buf, sizeof buf);

        recv(clientfd, buf, sizeof buf, 0);            //等待用户输入下一条命令

        if(strncmp(buf, "q", 1) == 0)

            break;

    }  

 

    fclose(fp);                                           //关闭文件

}

 

/*

 客户端写文件

 以只追加方式打开文件

 入口参数file->打开文件名,clientfd->客户端sock描述符

 */

void file_write(char *file, int clientfd)

{

    FILE *fp;                              //定义文件指针

    char buf[BUF_SIZE];

 

    fp = fopen(file, "a+");                          //以追加方式打开文件

    if(fp == NULL)

    {

        printf("%s %d", __FUNCTION__, __LINE__);

        perror("fopen");

        return ;

    }

   

//  strcpy(buf, "news_write(return)\n");

    send(clientfd, "\n------write(return)------\n", sizeof("\n------write(return)------\n")-1, 0);                //发送提示信息

    bzero(buf, sizeof buf);

 

    while(1)

    {

        if(recv(clientfd, buf, sizeof buf, 0) == -1)          //读取信息

        {

            printf("%s %d", __FUNCTION__, __LINE__);

            perror("recv");

            fclose(fp);  

            return ;

        }

 

        if(strncmp(buf, "return", 6) == 0)                     //用户写退出

            break;

 

        pthread_rwlock_wrlock(&rwlock);                         //读写锁上写锁

        fputs(buf, fp);                                           //写入文件

        pthread_rwlock_unlock(&rwlock);                         //读写锁解锁

 

    }

   

    fclose(fp);                                               //关闭文件

}

 

/*

 不同模式下处理函数

 客户端读信息和写信息

 入口参数 menu > 不同模式下菜单首地址

            file > 不同模式下要读写文件名

            client > 客户端信息

 */

void funthread(char *menu, char *file, LinkList *client)

{

    char buf[BUF_SIZE];

    int flag;

    LinkList *other;

   

    while(1)

    {      

        send(client->clientfd, menu, strlen(menu), 0);       //发送菜单

        send(client->clientfd,"input > ", sizeof("input > ")-1, 0);         //提示符

        bzero(buf, sizeof buf);

        read(client->clientfd, buf, sizeof buf);              //接收信息

       

        if(buf[0] == '\n')                                       //过虑单独的回车

            continue;

           

        if(strncmp(buf, "1", 1) == 0)                                   //读信息请求

        {

            //标记1表示正在读新闻

            client->flag += 10;              //标记在读模式

            file_read(file, client->clientfd);                 //发送信息内容

            client->flag -= 10;              //取消标记

        }

 

        if(strncmp(buf, "2", 1) == 0)                           //写信息请求

        {          

            file_write(file, client->clientfd);                    //

           

            if(client->flag == 1)        //1表示下在新闻模式

                flag = 11;             //11表示所有正在读新闻客户端

            if(client->flag == 2)        //2表示下在娱乐模式

                flag = 12;             //11表示所有正在读娱乐客户端

            other = head;

            while((other = Locate(other, flag)) != NULL)          //通知其他正在读新闻的客户

            {

                if(send(other->clientfd,"have new news\n", sizeof("have new news\n")-1, 0) == -1)           //提示客户

                {

                    printf("%s %d", __FUNCTION__, __LINE__);

                    perror("send");

                }

            }

        }

       

        if(strncmp(buf, "0", 1) == 0)                               //客户端返回上层界面

            break;

    }  

}

 

/*

 线程运行函数

 发送主菜单至客户端 客户模式选择

 */

void servthread(void *arg)         

{

    char buf[5]; 

    LinkList *client = (LinkList *)arg;

   

    while(1)

    {

        send(client->clientfd, menu, strlen(menu), 0);       //发送菜单

        send(client->clientfd,"input > ", sizeof("input > ")-1, 0);         //提示符

        bzero(buf, sizeof buf);

        read(client->clientfd, buf, sizeof buf);              //接收信息

       

        if(strncmp(buf, "1", 1) == 0)                           //进入新闻界面

        {          

            client->flag = 1;             //标记1表示进入新闻模式

            funthread(menu_news, "news.txt", client);

            client->flag = 0;             //取消标记

        }

       

        if(strncmp(buf, "2", 1) == 0)                           //进入娱乐界面

        {

            client->flag = 2;             //标记1表示进入娱乐模式

            funthread(menu_recreation, "recreation.txt", client);

            client->flag = 0;             //取消标记

        }

           

        if(strncmp(buf, "0", 1) == 0)                               //客户端退出

        {

            printf("Client Exit: %s %d\n",inet_ntoa(client->client_addr.sin_addr.s_addr), ntohs(client->client_addr.sin_port));

            send(client->clientfd, "exit", strlen("exit"), 0);       //退出信息给客户端

            close(client->clientfd);

            Delelem(head, client->clientfd);                           //在链表中删除客户端信息

            break;

        }

    }

    pthread_exit(0);      //线程退出

}

 

/*

 多线程并发服务

 */

int main(void)

{  

    LinkList *newclient;                 //

    pthread_t tid;                            //线程ID

 

    int sockfd;

    int clientfd;                                

 

    struct sockaddr_in client_addr;         //定义客户端口号和IP

    int length = sizeof client_addr;      

 

    sockfd = serv_init();                    //服务端程序初始化函数\创建套接字、绑定端口、监听网络端口\Sock文件描述符

    initlist(&head);                      //初始化链表头

   

    while(1)

    {

        /*接收链接请求*/

        clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &length);

        if(clientfd == -1)

        {          

            perror("accept");

            continue;

        }

        /*打印客户端IP及端口号*/

        printf("Client connet: %s %d\n", inet_ntoa(client_addr.sin_addr.s_addr),ntohs(client_addr.sin_port));

 

        /*动态分配内存保存客户端信息*/

        newclient = (LinkList *)malloc(sizeof(LinkList));                //动态分配空间

        newclient->clientfd = clientfd;                                 //保存客户端sock句柄

        newclient->client_addr = client_addr;                          //保存客户端IP及端口号

        Inselem(head, newclient);                                       //节点插入链表

        pthread_create(&tid, NULL, (void *)servthread, newclient);      //创建线程     

    }

 

    return 0;

}

 

 

/*

 客户端程序 多路复用

 客户端输入和接收信息

 

 */

 

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>

 

#define PORT  5555           //定义服务器端口号

 

int main(int argc, char *argv[])

{

    char buf[1024];                   //缓冲区

    int ret;

    int sockfd;                       //定义Sock文件描述符

    struct sockaddr_in serv_addr;   //保存服务端IP和端口信息

    fd_set readfds;                   //定义读文件描述符集

 

    if(argc != 2)     //命令行检查

    {

        printf("input error: [%s] [IP]\n",argv[0]);

        exit(0);

    }

 

    //创建套接字

    sockfd = socket(AF_INET, SOCK_STREAM, 0);        

    if(sockfd < 0)

    {

        perror("socket");

        exit(0);

    }

 

    //建立连接

    bzero(&serv_addr, sizeof serv_addr);

    serv_addr.sin_family = AF_INET;                             //使用TCP、IP

    serv_addr.sin_port = htons(PORT);                  //设定端口

    ret = inet_aton(argv[1], &serv_addr.sin_addr.s_addr);       //设定IP

    if(ret == 0)

    {

        perror("inet_aton");

        exit(0);

    }

    ret = connect(sockfd, (struct sockaddr *)&serv_addr, sizeof serv_addr);   //建立连接

    if(ret == -1)

    {

        perror("connect");

        exit(0);

    }

 

    while(1)

    {

        bzero(buf, sizeof buf); 

        FD_ZERO(&readfds);           //清空描述符集

        FD_SET(0, &readfds);     //将键盘fd加入读文件描述符集

        FD_SET(sockfd, &readfds);   //将sockfd加入读文件描述符集

        ret = select(FD_SETSIZE, &readfds, NULL, NULL, NULL);           //监视文件

        if(ret <= 0)

        {

            perror("select");

            continue;

        }

        else if(ret > 0)

        {

            if(FD_ISSET(sockfd, &readfds))                              //套接字文件状态发生变化

            {                      

                ret = recv(sockfd, buf, sizeof buf, 0);                   //接收数据

                if(ret == -1)

                {

                    perror("recv");

                    continue;

                }              

               

                if(strncmp(buf, "exit", 4) == 0)                       //收到退出信息

                    break;

               

                if(write(1, buf, strlen(buf)) == -1)

                {

                    perror("write");

                    continue;

                }  

            }          

           

            if(FD_ISSET(0, &readfds))

            {          

                read(0, buf, sizeof buf);                           //从键盘输入字符

                ret = send(sockfd, buf, strlen(buf), 0);              //发送数据

                if(ret == -1)

                {

                    perror("send");

                    continue;

                }

            }

        }      

    }

 

    //close(sockfd);              //关闭套接字

    return 0;

}