Linux-c 网络socket练习1
要求:
1、基于tcp/ip,自定义通讯协议,c-s架构,client和server端均用sh终端运行;
2、实现用户登录和注册功能,服务端返回操作码,如果失败,返回失败信息;
3、server端,使用链表实现用户数据在内存中的存储方式,使用文件IO实现服务端持久化;
扩展要求:
1、单点登录,并实现退出登录功能;
2、互斥,多客户端同时注册(同一用户名)。
一、程序设计
1、通讯协议
1.1、注册与登录数据报(定长报文):
客户端--->服务端
序号 | 字段名称 | 占用字节数 | 说明 |
---|---|---|---|
1 | 报文头 | 1 | 固定为: 0x11 |
2 | 操作码 | 1 | 1-登录,2-注册,其他-错误码 |
3 | 用户名 | 20 | ASCII字符串 |
4 | 密码 | 20 | ASCII字符串 |
5 | 报文尾 | 1 | 固定为: 0x22 |
服务端 ---> 客户端
序号 | 字段名称 | 占用字节数 | 说明 |
---|---|---|---|
1 | 报文头 | 1 | 固定为: 0x11 |
2 | 操作返回码 | 1 | 1-成功,其他-错误 |
3 | 错误描述 | 50 | ASCII字符串 |
4 | 报文尾 | 1 | 固定为: 0x22 |
2、持久化存储结构
使用以下结构体,将内存中的字节存储方式,直接写入到文件中,读取亦然。
typedef struct u{
char username[20];
char passwd[20];
}userinfo_t;
3、客户端的功能
1)带参数化运行 如 ./a.out 服务器ip 服务器端口号。启动时启用tcp连接服务器,如果失败,提升信息,退出程序;
2)成功连接上服务器后,显示操作界面及提示信息,简单的命令行终端即可(1-登录,2-注册,Q-退出客户端)
3)登录:输入用户名和密码,密码不可见,且用户名、密码不超过10位
4)注册:输入用户名、密码和确认密码,要求密码和确认密码一致,且用户名、密码不超过10位
5)若服务端主动断开连接,显示原因,退出程序
4、服务端的功能
1)带参数化的运行
2)用户名、密码持久化存储,注册成功,追加
3)操作失败,向客户端返回失败原因
4)若客户端断开连接,关闭对应的连接,但不退出程序
5)注册,用户名不允许重复
6)内存中的用户信息存储使用链表
5、目录结构
二、公共部分实现-头文件("接口")
1、userinfo.h
定义了用户信息的结构体
内存中用链表存储用户信息,及相关操作函数
文件持久化存储用户信息,及相关函数
typedef struct u{
char username[20];
char passwd[20];
}userinfo_t;
typedef struct n{
union{
userinfo_t data;
int len;
};
struct n *next;
}link_t,node_t;
// 链表操作函数
link_t* link_create();
void link_destroy(link_t* L);
void link_insertHead(link_t* L, userinfo_t user);
node_t* link_findByUsername(link_t* L, char* username);
// 文件操作函数
link_t* file_load();
void file_appendUser(userinfo_t user);
2、mynet.h
定义了客户端与通讯段的通讯报文 (未来如果需要添加断包与拆包的功能,也应该添加在此)
typedef struct{
char head;
char cmd;
char username[20];
char passwd[20];
char end;
}msg_send_t;
typedef struct{
char head;
char ret;
char errmsg[50];
char end;
}msg_ret_t;
void msg_send_init(msg_send_t* msg, char cmd, char* username,char* passwd);
void msg_ret_init(msg_ret_t* msg);
3、相关实现
3.1、userinfo.c
link_t* link_create(){
link_t* L=(link_t*)malloc(sizeof(link_t));
if(L == NULL){
fprintf(stderr,"链表初始化失败,malloc error\n");
return NULL;
}
L->len=0;
L->next=NULL;
return L;
}
void link_destroy(link_t* L){
node_t* p=L->next;
for(int i=0;i<L->len;i++){
node_t* temp=p;
p=p->next;
free(temp);
}
free(L);
}
void link_insertHead(link_t* L, userinfo_t user){
node_t *p=(node_t*)malloc(sizeof(node_t));
p->next=L->next;
L->next=p;
memcpy(&p->data, &user, sizeof(userinfo_t));
L->len++;
printf("链表:节点插入成功\n");
}
node_t* link_findByUsername(link_t* L, char* username){
node_t *p=L->next;
for(int i=0;i<L->len;i++){
printf("1:[%s],2:[%s]\n",p->data.username,username);
if(strcmp(p->data.username, username)==0){
return p;
}
p=p->next;
}
return NULL;
}
link_t* file_load(){
link_t* L=link_create();
//打开文件
if(access(FILE_PATH, F_OK)){//不存在
printf("文件不存在,新建文件\n");
//创建文件
int fd=open(FILE_PATH, O_WRONLY|O_CREAT|O_TRUNC, 0666);
close(fd);
//肯定无数据
return L;
}
printf("文件存在,读取文件数据\n");
int fd=open(FILE_PATH, O_RDONLY);
if(fd == -1){
perror("open error");
return NULL;
}
//读取数据
userinfo_t u;
ssize_t cnt;
while((cnt=read(fd,&u,sizeof(u)))>0 ){
//将读取数据插入到链表
printf("读取一次,读取了[%ld]个字节\n",cnt);
link_insertHead(L, u);
}
printf("read completed. cnt=[%ld]\n", cnt);
//关闭文件
close(fd);
return L;
}
void file_appendUser(userinfo_t user){
//打开文件, 程序运行到此处,文件一定存在
int fd=open(FILE_PATH, O_WRONLY|O_CREAT|O_APPEND, 0666);
if(fd ==-1){
perror("file_appenUser:文件打开失败");
return;
}
//追加数据
ssize_t cnt=write(fd, &user, sizeof(user));
printf("cnt=[%ld]个字节已经写入\n", cnt);
//关闭文件
close(fd);
}
3.2、mynet.c
void msg_send_init(msg_send_t* msg, char cmd, char* username,char* passwd){
msg->head=0x11;
msg->cmd=cmd;
strcpy(msg->username,username);
strcpy(msg->passwd,passwd);
msg->end=0x22;
}
void msg_ret_init(msg_ret_t* msg){
msg->head=0x11;
//
msg->end=0x22;
}
三、客户端实现
3.1 client.c
void login(int sk, msg_ret_t* msgRet);
void reg(int sk, msg_ret_t* msgRet);
//函数转移表
void (*funcArr[])(int,msg_ret_t*)={login,reg};
int main(int argc, const char *argv[])
{
if(argc != 3){//argv[1]:服务端ip argv[2]:服务端port
fprintf(stderr,"参数有误\n");
return -1;
}
//与服务端建立连接
//1.1 创建socket文件
int sk=socket(AF_INET, SOCK_STREAM, 0);
if(sk==-1){
perror("与服务端的连接建立失败 socket error");
return -1;
}
//1.2 connect
addr_in_t addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(atoi(argv[2]));
addr.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t addrlen=sizeof(addr);
if(connect(sk, (struct sockaddr*)&addr, addrlen)==-1){
perror("connect error");
return -1;
}
printf("与服务端成功建立连接\n");
//操作界面
while(1){
printf("*******************\n");
printf("\t1-登录\n");
printf("\t2-注册\n");
printf("\tQ-退出\n");
//int cmd;
//fscanf(stdin,"%d", &cmd);
int cmd=getchar();
getchar();//吸回车
//
msg_send_t msgSend={0};
msg_ret_t msgRet={0};
//调用函数转移表
printf("cmd=[%d],[%ld]\n", cmd, sizeof(funcArr)/sizeof(void*));
if(cmd=='Q'){
break;
}
if(cmd<49||cmd>49+sizeof(funcArr)/sizeof(void*)){
printf("错误的指令!请重新输入\n");
continue;
}
//调用
funcArr[cmd-49](sk, &msgRet);
if(msgRet.ret == 1){
printf("操作成功\n");
continue;
}
printf("操作失败!错误原因[%d][%s]\n", msgRet.ret,msgRet.errmsg);
}
close(sk);
printf("客户端已经关闭\n");
//
return 0;
}
void login(int sk, msg_ret_t* msgRet){
printf("\t\t【登录】\n");
char user[20];
printf("\t\t用户名:");
fgets(user,sizeof(user),stdin);
user[strlen(user)-1]=0;
char passwd[20];
printf("\t\t密码:");
fgets(passwd,sizeof(passwd),stdin);
passwd[strlen(passwd)-1]=0;
msg_send_t msg;
msg_send_init(&msg, 1, user,passwd);
ssize_t cnt=send(sk, &msg, sizeof(msg),0);
printf("发送[%ld]个字节\n", cnt);
if(cnt == -1){
perror("send error");
return;
}
cnt=recv(sk,msgRet,sizeof(msg_ret_t),0);
printf("收到[%ld]个字节\n", cnt);
}
void reg(int sk, msg_ret_t* msgRet){
printf("\t\t【注册】\n");
char user[20];
printf("\t\t用户名:");
fgets(user,sizeof(user),stdin);
user[strlen(user)-1]=0;
char passwd[20];
printf("\t\t密码:");
fgets(passwd,sizeof(passwd),stdin);
passwd[strlen(passwd)-1]=0;
char passwd2[20];
printf("\t\t确认密码:");
fgets(passwd2,sizeof(passwd2),stdin);
passwd2[strlen(passwd2)-1]=0;
if(strcmp(passwd,passwd2)!=0){
msgRet->ret=2;
strcpy(msgRet->errmsg,"两次输入的密码不一致\n");
return;
}
msg_send_t msg;
msg_send_init(&msg, 2, user,passwd);
ssize_t cnt=send(sk, &msg, sizeof(msg),0);
printf("发送[%ld]个字节\n", cnt);
if(cnt == -1){
perror("send error");
return;
}
cnt=recv(sk,msgRet,sizeof(msg_ret_t),0);
printf("收到[%ld]个字节\n", cnt);
}
3.2、客户端的Makefile
因为涉及.c文件在不同目录下的编辑,为了方便与练习,贴出Makefile:
OUT=client.out
CC=gcc
OBJS=${patsubst %.c,%.o,${wildcard *.c} ../common/mynet.c ../common/userinfo.c}
all:${OBJS}
gcc -o ${OUT} $^
%.o:%.c
gcc -c -o $@ $^
clean:
${RM} -f ${OBJS} ${OUT}
四、服务端实现
4.1 server.c
void handle(link_t* L,msg_send_t* msg, msg_ret_t* ret){
msg_ret_init(ret);
switch(msg->cmd){
case 1:{
//login
//node_t* p=link_findByUsername(L, msg->username);
node_t *p=link_findByUsername(L, msg->username);
if(p == NULL){//用户不存在
printf("111\n");
ret->ret=2;
strcpy(ret->errmsg,"该用户不存在");
return;
}
printf("222\n");
//用户存在,校验用户密码
if(strcmp(p->data.passwd, msg->passwd)==0){
ret->ret=1;
strcpy(ret->errmsg,"登录成功");
return;
}
ret->ret=2;
strcpy(ret->errmsg,"密码错误");
return;
}
case 2:{//reg
//查询用户是否已经存在
node_t *p=link_findByUsername(L, msg->username);
if(p!=NULL){
ret->ret=2;
strcpy(ret->errmsg,"注册失败,该用户已经存在");
return;
}
//若不存在,将用户添加到文件中
userinfo_t uuu;
strcpy(uuu.username, msg->username);
strcpy(uuu.passwd,msg->passwd);
file_appendUser(uuu);
//将用户信息,追加到内存中
link_insertHead(L, uuu);
ret->ret=1;
strcpy(ret->errmsg,"注册成功");
break;
}
default://不支持的操作
ret->ret=2;
strcpy(ret->errmsg,"不支持的操作,目前仅支持1-登录,2-注册操作");
break;
}
}
int main(int argc, const char *argv[])
{
//入参校验
if(argc != 3){
fprintf(stderr,"输入的参数有误\n");
return -1;
}
int port=atoi(argv[2]);
//
//从文件读取存储信息
link_t* L=file_load();
//
//创建服务端socket文件
int serverFD=socket(AF_INET, SOCK_STREAM, 0);
printf("服务端套接字文件创建完成,serverFD=[%d]\n", serverFD);
//
//ip信息与socket文件绑定
addr_in_t addr={0};
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(serverFD, (addr_t*)&addr, sizeof(addr))==-1){
perror("bind error");
return -1;
}
//listen
if(listen(serverFD, 10)==-1){
perror("listen error");
return -1;
}
while(1){
//accept获取客户端socket文件
addr_in_t client_addr={0};
socklen_t client_len=sizeof(client_addr);
int clientFD=accept(serverFD, (addr_t*)&client_addr, &client_len);
//printf("客户端[%d][%s]已经建立连接\n", clientFD, inet_ntoa(client_addr.sin_addr.s_addr));
//业务逻辑
char buf[64];
while(1){
//ssize_t cnt=recv(clientFD,buf,sizeof(buf),0);
//printf("cnt=[%ld],收到消息:%s\n", cnt,buf);
//if(cnt==0){
// if(close(clientFD)==-1){
// perror("clientFD关闭失败");
// break;
// }
// printf("clientFD=[%d]已经关闭\n", clientFD);
// break;
//}
//读取报文
msg_send_t msg;
ssize_t cnt=recv(clientFD, &msg, sizeof(msg),0);
printf("从客户端收到了[%ld]个字节\n", cnt);
//业务处理
msg_ret_t ret;
handle(L,&msg, &ret);
//返回处理结果
cnt=send(clientFD, &ret, sizeof(ret),0);
printf("ret=[%d],errmsg=[%s]\n", ret.ret, ret.errmsg);
printf("向客户端发送了[%ld]个字节的数据\n", cnt);
}
//close客户端socket文件
}
return 0;
}
4.1 服务端的Makefile
注意与客户端的Makefile的区别:
OUT=server.out
CC=gcc
OBJS=${patsubst %.c,%.o,${wildcard *.c} ../common/mynet.c ../common/userinfo.c}
all:${OBJS}
gcc -o ${OUT} $^
%.o:%.c
gcc -c -o $@ $^
clean:
${RM} -f ${OBJS} ${OUT}
五、运行效果
略