Linux下的Tcp通信项目范例【demo】

一,适合阅读对象:2-4个月的初学者(C语言编程方向)

二,项目内容:

设计一个可以符合多用户进行线上查阅乐器的商城,要求可以多个用户查看,管理员可随时修改内容,普通用户仅可查看,需要相应密码权限操作。

三,项目需要的技术:

tcp(socket通信),IO多路复用,sqlite3数据库引用,makefile工程管理(已经说腻了的技术)

四,设计思路:

1,创建登录界面和注册界面。2,设计普通用户和管理员账户界面。3,设计界面功能。

注:正文内主要讲解登录和注册,其他的主讲思路,需要理解意思请详细查看。接下来开始讲解。

----------------------------------我是分割线--------------------------------------------------

一,创建登录界面和注册界面

思维路径(可能需要用到的技术):①创建管理用户账号密码的数据库。②输入密码时将数据库内容拉出来与用户输入内容做对比,正确进入,失败则退出。③因为主要是为了让用户进行查看,所以密码忘了重新再创建一个就行,不写找回密码(只是自己比较懒-,-)。【总头文件在最底下

①先来在服务器bin目录下定义用户信息数据库。为了简单好记,直接就如下(这条代码不重要,可以忽略,但是下面那个sqlite3数据库下载方式要记住):

create table code(username text primary not null,passwrod text);
//在数据库内部直接创建数据表code,定义关键元素一username(用户名),
//元素二(用户密码) .
//注意:此路径在命令行服务器(server)的运行文件目录(bin)下,
//我们创建数据库的名字sqlite3 code.db 

如果没有sqlite3数据库,建议去菜鸟教程下载,

下载完后请输入sudo apt-get install  libsqlite3-dev获取sqlite3库文件。

注意:这里是在命令行创建的用户信息数据库,如果有商家使用,则我们需要在注册时候服务器自动创建一个

②接下来要做的,就是写子函数让用户去注册密码。(有密码才能注册!)如下为【客户端】:

//函数功能,发送用户注册的账户密码信息至服务器
//参数1:参数为客户端socket文件描述符
//返回值:成功返回0 失败返回-1
int  creatpassword(int sock) //
{
char all[50]={0};//用来收录用户输入内容
printf("请输入您需要注册的账号与密码 格式【账号 密码#】\n");//此格式的目的是为了解析用户输入内容,就像输入门禁密码都要按#号一样,我们的规定是空格前为账号,空格后到#号为密码.
fgets(all,sizeof(all),stdin); //获取缓冲区内容
send(sock,all,sizeof(all),0); //发送信息至服务器
memset(all,0,sizeof(all));//
recv(sock,all,sizeof(all),0);//用来接收服务器若注册成功,发送的返回消息
if(strcmp(all,"注册成功")==0) {printf("注册成功,请退出程序重新进入\n");return 0;}
if(strcmp(all,"注册失败")==0) {printf("注册失败,请联系管理员查看\n");return 0;}
return 0;
}

下为服务器端注册密码子函数【服务器端 】

//函数功能,接收客户端发送的账号密码存储至数据库
//参数1:服务器监听socket接收到文件后,生成对应的文件描述符。参数2:用户输入的账户名
//参数3:用户输入的密码
//返回值:成功返回0 失败返回-1
int createuser(int i,char* username,char* password)
{
sqlite*db=NULL;//用来引导命令的数据库指针
char  sql[100]={0};//用来收纳放入账号密码至数据库的命令
int tc = sqlite3_open("yuequ.db",&db);if(tc<0)printf("注册数据库打开失败!\n");//打开数据库
int tp = sqlite3_exec(db,"create table if not exists code(username text primary key not null,password text)");//创建一个数据表存放用户账户密码,若存在则不会创建,不存在则新建
if(tp<0){printf("用户数据表创建异常!");return -1;}
int ts=sprintf(sql,"insert into code ('%s','%s')",username,passwrod); //存储用户输入的内容至用户数据表中
if(ts<0) {printf("用户数据存储失败!\n");return -1;}
rturn 0;
}

注册操作完成了,那至少我们要把需要传输的桥(tftp)搭起来,所以我们先创建一下需要的socket文件。

接下来为客户端socket创建【客户端】

//函数功能,客户端sock文件
//参数一,命令行输入服务器的IP 参数2,命令行输入服务器端口号
//返回值,成功返回socket文件描述符,失败返回-1 
int creatsock(argv[1],argv[2])
{
int fa = socket(AF_INET,SOCK_STREAM,0);if(a<0){printf("客户端套接字文件创建失败!\n");rturn -1;}

struct sockaddr_in FF;//定义客户端链接服务器时,需要用到结构体
FF.sin_familty=AF_INET;//赋值传输层协议给结构体family
FF.sin_add.s_addr = inet_addr(argv[1]);//将输入的IP从主机字节序转化为网络字节序,然后赋值给结构体
FF.sin_port= htons(atoi(argv[2]));//先将输入的端口号从字符串转为整形,再转化为网络字节序,赋值给结构体  两个都一样,我们输入时候都是主机字节序,不转化的话传过去服务器不认识
int fb=connect(socket,(struct sockaddr *)&FF,sizeof(FF));//与服务器进行链接
if(fb<0){printf("主机端尝试发送链接失败\n");return -1;}
return fa;
}

接下来为服务器端口的socket文件创建及接收绑定 【服务器端

//函数功能:实现与客户端的tcp链接
//参数1:需要绑定的Ip号 参数2:需要绑定的端口号
//返回值:成功返回socket文件描述符,失败返回-1
int tcp_server(argv[1],argv[2])
{
int ta=socket(AF_INET,SOCK_STREAM,0);//参数1:传输协议 参数2:流式套接字的标识符 参数3:与参数2组合使用以构成特殊协议。本项目不涉及,所以为0;
if(ta<0) {printf("服务器socket文件创建失败!\n");return -1;}
struct sockaddr_in BB;//用户服务器自身绑定时,识别的第二个参数
BB.sin_addr.s_addr = inet(argv[1]);
BB.sin_port = htons(atoi(argv[2]));
BB.sin_family = AF_INET;
int tb=bind(ta,(struct sockaddr*)&BB,sizeof(BB)); //操作系统规定第二个参结构体类型没有in,又规定结构体创建时必须有in,所以需要强行转换,以让操作系统识别到整个结构体。
if(tb<0){printf("绑定失败!\n");return -1;}
int tc=listen(ta,1024);//监听服务器是否有其他接入请求,最多可支持1024个客户端
if(tc<0) {printf("监听树立失败!\n");return -1;}
return ta;
}

③ 注册完成了,并且可以连接了,那是不是就是可以判断用户输入的密码是否正确呢? NO!NO!NO! 要知道我们要做的可是一个服务器同时接收多个客户端,现在我们只是仅仅可以监听出来1024个请求,但无法接起来,所以下一步,我们需要创建IO多路复用,获得多个客户端同时连接服务器的功能。具体如下【服务器端】:

int main{int argc,char *argv[]}
{
int Newfd=0; //用来接入新客户端的文件描述符
int tp = tcp_server(); //接收服务器自身的socket文件
struct sockaddr_in II; //用来接收客户端传递的结构体以搭建tftp桥梁
II.sin_family=AF_INET;
II.sin_addr.s_addr = inet_addr(argv[1]);
II.sin_port=htons(atoi(argv[2]));

fd_set table;fd_set temp;//创建两个套接字表格,以存放接入的客户端socket文件描述符及接听功能
fd_ZERO(&table); fd_ZERO(&temp);//腾空套接字表格内数据
fd_SET(tp,&table);//将初始的套接字描述符放到table表格内
while(1) //循环监听外部接入请求
{ 
temp = table;//将参照表格内数据放置动态表格
struct timeval tv;//建立未接入客户端时,服务器正常扫描一次运行的时间间隔
tv.tv_sec = 10;//结尾
tv.tv_usec=0;  //开始
 int ta = select(FD_SETSIZE,&temp,NULL,NULL,&tv);//每空十秒就将扫描一次temp表,共扫描1024个
//空位,FD_SETSIZE=1024;
if(ta<0) {printf("运转异常,多路复用IO创建失败!\n");return -1;}
if(ta==0){printf("程序运转中....\n");}
if(ta>0) //这个时候就来客户端请求了,但不知道是新客户端请求接入,还是旧客户端发送数据
{    
    for(int i= 0;i<FD_SETSIZE;i++)
     {
       if(FD_ISSET(i,&temp))  //扫描目前temp表有几个文件描述符,返回文件描述符的数字
         {
              if(i==tp)  //返回的数字和初始化的数字相等
            { 
                   int Newfd=connect(tp,(struct sockaddr *)&II,sizeof(II));
                   if(Newfd<0){printf(新客户端接入异常\n);}
    printf("客户端接入成功,IP为%s  ,端口号为%d\n",ntoa(II.sin_addr),ntohs(II.sin_prot));
                     FD_SET(Newfd,&table);//将新客户端文件描述符放到参照表里
            }
              else
               {
                 //填入运转子函数,发现接入的是旧客户端发送数据,就可以开始分析工作了
               }
         }
     }
}
}
}

做完上面这一步,那我们现在既可以多端互通,也可以正常链接了。

接下来就是解析我们上面说过的,用户链接上后开头发送了一个【账号空格密码#】的内容,我们需要解析,收到数据后,第一件是就是先去解析命令。所以无论发过来的是账号密码或者其他类型命令,都是以这个格式进行解析的。接下来我们来写入解析函数

④写入解析子函数,解析用户发过来的数据内容 【服务器端】(算法我就不解释了,大家自己看)

//函数功能,解析用户命令
//参数1,完整用户命令 
//参数2,外部引入的空缓存区,存放命令1 
//参数3,外部引入的空缓存2用来存放命令2
//返回值: 成功返回0 失败返回-1
int parcode(char *all,char *b,char*c)
{
   char *str=all;
	while(1)
	{
		if(*str==' ')
		{
			*cmd = 0;
			 str++;
			 break;
		}
		*cmd=*str;
	     cmd++;
		 str++;
	}
	while(1)
	{
		if(*str == '#')
		{
			*name = 0;
			break;
		}
		   *name=*str;
		   str++;
		   name++;
	}
	return 0;
}

⑤引入管理其他子函数的组长子函数,以方便所有子函数工作流程为: 获取数据->解析数据->对应操作子函数->发送用户端需要的内容【服务器端】如下思路版为:

//函数功能,管理所有子函数,使服务器其正常运转
//参数:从主函数里收纳的客户端socket描述,内含客户端发送的指令
//返回值:成功0,失败-1
int func(int i) 
{
   char all[50]={0};  //用以存放用户开头发送的数据 
   recv(i,all,sizeof(all),0);//用来接收用户发送的数据
   char a[20]={0}; char b[20]={0};  //分别存放用户解析数据后分出来的命令A 和命令B   
   parcode(all,a,b); //解析命令发送至a与b
   //解析完命令后,就可以开始操作了,根据发送的命令,去执行相应子函数即可。
   // 只举一个例子,刚开始我们发送了账号密码 所以发送账号密码之前我需要发送一个命令到
    服务器,然后进入存储密码的子函数,即上面的
                                int creatuser(char*all,char*username,char*password)
    所以开头客户端连接后就需要先发送一个命令让主函数解析,进入相应的子函数
    然后服务器子函数再次与客户端进行交互即可实现需求
   

}

可以看到服务器详细操作如下:【服务器端】

int func(int i)
{
	sqlite3 *db=NULL;char all[100]={0};char cmd[100]={0};char name[100]={0}; 
	char B[50]={0}; 
    char C[50]={0};   //用来接收账号密码
	memset(cmd,0,sizeof(cmd));
	memset(all,0,sizeof(all));
    int o =0;
	recv(i,all,sizeof(all),0);  if(signal(SIGPIPE,SIG_IGN)==SIG_ERR){return 0;}
	if(*all!=0)    //避免用户发送空文件导致异常数据,导致解析失败出现段错误关闭服务器
	{
	      while(o<100)//避免用户发送命令后没有添加#号,导致搜寻错引起段错误
		  {
			  if(all[o]!='#')
			  {
				  o++;
			  }
			  else if(all[o]=='#')
			  {
		       parsecode(all,cmd,name);//基础命令解析
	           printf("读取基础命令为%s\n",cmd);
				  break;
			  }
		  }
	}
	if(strncmp(cmd,"create",6)== 0)
	{  
		recv(i,B,sizeof(B),0);
		sleep(1);
		recv(i,C,sizeof(C),0);
		printf("创建的账号为%s  密码为%s\n",B,C);
		code(i,B,C); //这个code即是录入密码至数据库的子函数
		char NNN[100] ={0};
        strcpy(NNN,"账号密码录入成功");
		send(i,NNN,strlen(NNN),0);
		memset(B,0,sizeof(B));
		memset(C,0,sizeof(C));
	}
	else if(strncmp(cmd,"coming",6)==0)  //基础命令为用户需要输入密码进入程序
	{ 
		recv(i,B,sizeof(B),0); 
		sleep(1);
		recv(i,C,sizeof(C),0);
		int tc = sqlite3_open("./code.db",&db);
		if(tc<0)
		{
			perror("密码检测失败!\n");
		}
		char sql[100] = {0};
		sprintf(sql,"select *from code where username ='%s' and password ='%s' ",B,C);
		printf("%s\n",sql);
		char *nn=NULL;  
		sqlite3_exec(db,sql,CBcode,&i,&nn);  //这里的第三个参数为CBcode子函数,如果数据搜寻到发送的数据,则进入CBcode运转,未搜寻到则直接跳过sqlite3_exec函数 
		send(i,"账号密码不存在或出错!",40,0);//发送错误信息至客户端
		memset(B,0,sizeof(B)); //无论如何,上面的send都会被发送至客户端,所以成功了则客户端需要接收两次消息,失败了则接收一次。那么成功了则需要让客户端显示成功这一条收到的信息,隐藏失败的这一条。若失败了则直接打印失败就可以,同样用strncmp进行判定
		memset(C,0,sizeof(C));
	}

关于服务器端sqlite3_exec里面的回调函数CBcode如下:【服务器端】

int CBcode(void *args,int columnNum,char ** columValue,char **columnName)
{
    int * pid = (int *)args;   //强转以拿到与对应客户端沟通的socket文件描述符
	int socket = *pid;  //通信fd
	myfind MYfind;
	memset(&MYfind,0,sizeof(myfind));
	strcpy(MYfind.username,columValue[0]); //赋值数据库内第0列找到的数据a【找到才会进来,找不到这个函数就不会运行,也不会发送任何东西。为什么是第0列,因为我们创建的时候就把username放定义在了第0列】
	strcpy(MYfind.password,columValue[1]); //赋值数据库内找到的数据b
	printf("结构体搜寻账号为%s,密码为%s\n",MYfind.username,MYfind.password);
    send(socket,&MYfind,sizeof(myfind),0);
	return 0;
}

这里再展示一下客户端main.c流程 【客户端】

#include "../include/main.h"
int main(int argc, char *argv[])
{
	char buf[200]={0};  
	int sock = MYSOCKET(); //创建套接字文件
	connectserver(sock,argv[1],argv[2]);
	recv(sock,buf,sizeof(buf),0);  //接收服务器提示
	printf("%s\n",buf);
	int OP;
	while(1)
	{ 
		printf("|----------------------------------------------|\n");
		printf("|选项1-----------输入账号密码------------------|\n");
		printf("|选项2-----------注册账号密码------------------|\n");
		printf("|选项0--------------退出程序-------------------|\n");
		printf("|______________________________________________|\n");
		scanf("%d",&OP);	
		if(OP == 0)
		{return 0;}
		switch(OP)
		{
		case 1: 
			{
				send(sock,"coming #",10,0);  //基础命令,其他收发与服务器对应
				printf("请输入您的账号和密码,格式为【账号 密码#】\n"); 
				getchar();
				int S =  sendfile1(sock);//登陆账号密码的子函数
				if(S!=0)
				{
					printf("请重新输入账号密码!\n");
					return -1;
				}
				break;
			}
		case 2:  {
					 send(sock,"create #",10,0); 
					 printf("输入账号密码,格式【账号 密码#】\n");
					 sleep(1);
					 getchar();
					 char use[1000]={0}; char a[20]={0}; char b[20]={0}; 
					 fgets(use,sizeof(use),stdin);
					 parsecode(use,a,b);
					 printf("输出账号密码分别为%s %s\n",a,b);
					 send(sock,a,sizeof(a),0);
					 sleep(1);
					 send(sock,b,sizeof(b),0);
					 memset(use,0,sizeof(use));
					 recv(sock,use,sizeof(use),0);
					 printf("%s\n",use);
					 sleep(2);
					 printf("注册识别完成\n");
					 return 0; //注册
				 }
		} 
		char ceshi[50]={0};
		recv(sock,ceshi,sizeof(ceshi),0); //用来接收成功或单一变量生成时,发送的错误信息
        但不打印其信息,对应的是case1输入密码
		break;
}

里面的输入密码的sendfile1为:【客户端】

#include "../include/main.h"
//函数功能,发送账号密码至服务器进行对比
//参数一,套接字文件 
//返回值:成功0 失败-1
int sendfile1(int sock)
{
	char buf[40]={0};
	char usename[20]={0};
	char code[20]={0};
	fgets(buf,sizeof(buf),stdin);
	parsecode(buf,usename,code);//客户端内部解析用户名命令发送
	printf("输出账号密码分别为%s %s\n",usename,code);
	send(sock,usename,strlen(usename),0);  //信息1 输出的账号
	sleep(1);
	send(sock,code,strlen(code),0); 信息2:输出的密码
	sleep(1);
	myfind MYfind;  //声明接收结构体,结构体内容为从数据库拉出来的账号密码信息,没有对比到或者输入错误返回来的肯定是无法通过下面的操作
	memset(&MYfind,0,sizeof(myfind));
	int tc = recv(sock,&MYfind,sizeof(myfind),0);  //收账号信息
	if(tc<0) printf("接收失败\n");
	printf("用户名为%s,接收账号为%s\n",MYfind.username,MYfind.password);
 	
   if (strncmp(usename,MYfind.username,strlen(usename)!=0)) //对比服务器从数据库搜寻的内容和输出的内容
	{
		printf("用户名识别异常!\n");
		return -1;
	}
	printf("用户名识别成功!\n");

	if (strncmp(code,MYfind.password,strlen(code)!=0))
	{
		printf("密码识别异常!\n");
		return -1;
	}
	printf("密码识别成功!\n");
	return 0;
}

以上就是登录注册界面详细信息,其他的子函数只需要发送命令,对比命令,执行子函数再服务器,然后交互信息即可。最后给大家看一下详细执行结果

-----------------------------------------------------我是分割线-------------------------------------------------------------

二,展示界面

实现功能1 :多端查看

实现功能2:注册和密码登录

 三,乐器商城功能

流程:识别基础命令->识别二级命令->执行二级命令->两边互通->实现需求 

综合以上,即使本次乐器商城的思路与一些实际操作例子。

此次操作的库头文件如下:我就不一一解释了,建议大家查询系统手册。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sqlite3.h>
#include <sys/time.h>
#include <pthread.h>
#include <signal.h>

如有疑问、错误欢迎私聊纠正。祝大家敲的愉快!!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值