/*
*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 *p = L->next; //从下一个节点开始查找
while(p != NULL && p->flag != x) //遍历链表
p = p->next;
return p;
}
/*
* 删除节点 按客户端sock文件描述符clientfd
*/
void Delelem(LinkList *L, int x)
{
LinkList *p1, //要删除节点前一个节点的位置
*p2; //要删除节点后一个节点的位置
LinkList *p = L; //从下一个节点开始查找
while(p != NULL && p->clientfd != x) //遍历链表
{
p1 = p;
p = p->next;
}
if(p == 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;
}