目录

项目功能:

1.注册

2.登录

3.查看在线列表

4.私聊

5.下线

6.群聊

7.传文件

技术实现:

具体步骤:

1.首先进行初始化。

2.初始化完成之后,客户端就会创建两个线程,一个线程用来发送信息给服务器、一个用来接收服务器发送的信息。

发送信息给服务器:

a.注册(`0`):

b.登录(`1`): 

c.查看在线列表(`2`):

d.私聊(`3`):

e.下线(`4`):

f.群聊(`5`):

发送群聊信息:

接收群聊信息:

g.传文件(`6`):

接收服务器发送的信息:

a.查看在线列表(`2`):

b.私聊(`3`):

c.传文件(`6`):

项目效果:

服务器收到客户端连接请求

注册(若已经注册过服务器会提示用户名已存在) ​编辑

登录(昵称和密码错误服务器都会做出相应提示) ​编辑

 查看在线列表​编辑

 私聊

 (若目的客户端不在线或输入昵称有误则提示)

传文件 ​编辑

 群聊​编辑

​编辑 下线

源码:

chatroom_client.c

chatroom_server.c


项目功能:

1.注册

客户端上若有用户想注册,需要查看功能查询表,找到注册对应的命令号(`0`),在终端输入这个命令号,客户端会出现提示信息(请输入您的昵称(以#结束):),用户只需在终端上输入自己心仪的昵称,后面加个#即可,随后客户端又会出现提示信息(请输入您的密码:),用户输入自己的密码(长度不限)即可。

客户端完成上面这些操作之后就会发送数据包给服务器,服务器就会将这个用户的信息存到注册数组里面,出现提示信息,用户注册成功,服务器也会做一些操作来判断该是否重命名,若是则会提示“该用户名已存在,请重新注册!”。

2.登录

客户端上若有用户想登录,需要查看功能查询表,找到注册对应的命令号(`1`),在终端输入这个命令号,客户端会出现提示信息(请输入您的昵称(以#结束)和密码:),用户只需按照提示输入相应的信息即可。

随后客户端将数据包发送给服务器,服务器就会判断该用户是否注册?密码是否正确?是否有用户已经登录?如果没有注册就会提示“输入昵称错误,您还没有注册,请先注册再登录!”;   如果密码错误就会提示“密码错误,请重新输入!”;如果用户已经在一个终端上登录过了,则不能再这个终端上进行登录会提示“用户已经登录过了!”。

如果上面这三个检查都没有问题,则服务器会提示“用户登录成功,欢迎上线!”,并将该用户的信息存储到登录数组里面。

3.查看在线列表

客户端上若有用户想查看在线列表,需要查看功能查询表,找到注册对应的命令号(`2`),在终端输入这个命令号,客户端会出现提示“稍等......”,客户端将数据包发送给服务器。

服务器接收之后知道客户端是想查看在线列表,于是会将登录数组里面的信息发送给客户端,客户端接收之后就可以得到在线列表了(根据登录数组的连接套接字是否为零来判断用户是否在线)。

4.私聊

客户端上若有用户想私聊,需要查看功能查询表,找到注册对应的命令号(`3`),在终端输入这个命令号,客户端会出现提示“请输入您想聊天的客户端对象昵称以及聊天内容(用空格隔开):”,客户端输入自己想聊天的对象以及要发送的信息,如果这个聊天的对象在登录数组中没有找到,就说明目的客户端此时不在线或者是输入昵称有误;如果找到了就说明目的客户端此时在线。

随后就可以开始组建发送数据包了,  因为需要私聊,所以需要将目的客户端和服务器的连接套接字填充到这个发送数据包里面(填充的方法有多种:第一种是用一个字符变量来存储目的客户端和服务器的连接套接字(前提是这个套接字不是很大),这是一个整形,长字节赋值给短字节,遵循低字节拷贝高字节全部丢弃的原则;第二种是因为套接字是一个整形,有四个字节32位,可以将它分四次打包进要发送的数据包里面,>>不同位数之后,分别&上0xff,即可得到这个套接字某个字节上的原始八位数字,因为&上一个1原来的数值不会发生变化)然后就可以发送数据包给服务器了。

服务器收到数据包后会解析出接受者的套接字(这个方法也有多种: 第一种是用一个整型变量来存储刚刚客户端发来数据包的字符数;第二种对应上面填充的第二种方法,将分成的四个字符合成一个整形,需要<<恢复),解析完毕之后,服务器会出现提示这是客户端想发送给谁的信息?信息是什么?如果遍历完了登录数组都没有找到目的客户端,服务器就会提示服务器找不到目的客户端。

随后服务器也会组建一个发送的数据包,在数据包里面需要填充发送客户端和服务器的连接套接字(如何发送?请参考上面的两种方法)。

5.下线

客户端上若有用户想下线,需要查看功能查询表,找到注册对应的命令号(`4`),在终端输入这个命令号,客户端会出现提示稍等,发送相应的数据包给服务器。

服务器接收之后就会知道有客户端想下线了,随后会将该下线的客户端所在登录数组的位置用后面的用户填充(也可以说是挤下来)。

6.群聊

客户端上若有用户想群聊,需要查看功能查询表,找到注册对应的命令号(`5`),在终端输入这个命令号,客户端会出现提示“欢迎进入多播聊天室!”。

客户端会创建两个线程,一个线程用来接收群聊信息,一个用来发送群聊信息(群聊跟服务器基本上没有什么关系)。

在发送信息这里需要设置一个互斥锁、一个条件变量和一个共享变量,因为命令号的输入是在终端,群聊信息的输入也是在终端,会造成共享资源的抢占。如何设置?在后面具体步骤会说明。

发送信息这里会提示第一个字符输入q就会退出群聊,如果输入的字符串里面第一个字符不是q,就会将发送的信息群聊发送,这里用多播来实现;如果输入的字符串里面第一个字符是q,就会调用pthread_cancel函数,这个函数是用来取消掉一个线程的,并且是在别的线程里调用,这样之后其他的群发信息该终端就收不到了。

接收信息就很简单了,直接用recvfrom函数解析出多播信息来自于谁?信息是什么?

7.传文件

客户端上若有用户想传文件,需要查看功能查询表,找到注册对应的命令号(`6`),在终端输入这个命令号,客户端会出现提示“我想传输文件”,传输文件跟私聊的方法其实本质上是一样的。客户端先提示“请输入您想传输文件的客户端对象昵称以及文件名(用空格隔开):”, 根据昵称来判断该目的客户端是否在线。

和私聊唯一的区别就是传输文件需要打开要传输的文件并读取里面的内容,然后再将目的客户端和服务器的连接套接字以及文件的内容都写入到要发送的数据包里面。服务器收到数据包之后就会解析出接受者的连接套接字,方法如上私聊,服务器也需要组建一个发送数据包,将发送者和服务器的连接套接字以及文件内容填充进去。

目的客户端收到了服务器发来的数据包,会提示要收文件啦,以“w+”(读写方式打开,若文件不存在,则创建,打开后文件的内容截短)打开,然后将接收的内容写入到这个打开的文件里面,传输文件成功。


技术实现:

线程的创建、线程的退出

互斥锁、条件变量、共享变量

文件IO、系统IO

文件的打开、读、写操作

数组(注册、登录)

TCP UDP 多播


具体步骤:

1.首先进行初始化。

初始化客户端,用socket函数来创建一个客户端自己的套接字,指定参数为AF_INET(代表IPv4协议族)、SOCK_STREAM(代表流套接字)、0(代表不知名的私有应用协议),再用connect函数连接指定地址的服务器,注意,这里要将输入的端口号先用atoi函数变为整数,然后再用htons函数将其从主机字节序变成网络字节序;将输入的IP地址用inet_addr函数变为二进制网络地址。

初始化服务器,socket函数来创建一个服务器自己的套接字,指定参数为AF_INET(代表IPv4协议族)、SOCK_STREAM(代表流套接字)、0(代表不知名的私有应用协议),再用bind函数把一个套接字和网络地址绑定起来,注意,这里也要将输入的端口号先用atoi函数变为整数,然后再用htons函数将其从主机字节序变成网络字节序;将输入的IP地址用inet_addr函数变为二进制网络地址。然后再用listen函数让套接字进入监听状态,再用一个死循环让服务器一直都能接收来自客户端的连接请求,用accept函数来接收。客户端和服务器连接成功之后,服务器就会创建一个线程来处理客户端发送的信息并解析出相应的操作,发送给对应的目的客户端。

2.初始化完成之后,客户端就会创建两个线程,一个线程用来发送信息给服务器、一个用来接收服务器发送的信息。

发送信息给服务器:

a.注册(`0`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号REGISTER,这是第一个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“请输入您的昵称(以#结束):”,用户只需在终端上输入自己心仪的昵称,后面加个#即可,接下来计算出昵称的长度,这样方便继续输入密码加入发送数据包。随后客户端又会出现提示信息“请输入您的密码:”,用户输入自己的密码(长度不限)即可,客户端完成上面这些操作之后就会发送数据包,用write函数实现。

由于服务器一直在接收来自客户端发送的信息,当接收到数据包时,用read函数接收,服务器会将该数据包的第一个字节取出来查看,分别是对应什么命令号。在这里对应的是REGISTER命令号,此时会在服务器上显示用户注册成功,接下来继续读取数据包里面的内容,用#来分隔昵称和密码,将昵称,密码,客户端和服务器的连接套接字都填充到注册数组里面。

这里还会有一个操作检查注册是否重命名,如何检测?设置一个全局变量si,初始化为零,每有一个用户注册之后,它的值就会加一,用一个for循环来判断已经注册过的用户里面是否有这个名字(strcmp(sign[si].name, sign[i-1].name) == 0),如果有就会提示“该用户名已存在,请重新注册!”。

b.登录(`1`): 

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号LOGIN,这是第二个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“请输入您的昵称(以#结束)和密码:”,用户只需按照提示输入相应的信息即可,这里的的内容会被填充到发送数据包第一个字节之后。

随后客户端将数据包发送给服务器,服务器就会判断该用户是否注册?密码是否正确?是否有用户已经登录?

如果没有注册就会提示“输入昵称错误,您还没有注册,请先注册再登录!”(这里的判断是用了一个for循环,因为我的登录数组和注册数组设置的大小都是10,一个个比较看注册数组是否有了该昵称);   如果密码错误就会提示“密码错误,请重新输入!”;如果用户已经在一个终端上登录过了,则不能再这个终端上进行登录会提示“用户已经登录过了!”(这里的判断也是用了一个for循环,设置一个全局变量,它的初始值为零,每当有用户登录之后,它的值就会加一,比较条件就是在这几个人里面来比较登录数组里面是否已经有了该昵称?(strcmp(login[li].name, login[i-1].name) == 0))。

如果上面这三个检查都没有问题,则服务器会提示“用户登录成功,欢迎上线!”,并将该用户的信息存储到登录数组里面。

c.查看在线列表(`2`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号ONLINE,这是第三个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“稍等......”,然后会将这个数据包发送给服务器。

服务器收到数据包之后就会检测到户端想要查看在线列表,于是服务器也会创建一个发送数据包,该数据包第一个字节填充命令号ONLINE,再用memcpy函数将登录数组里面的所有内容都填充到发送数据包第一个字节之后,用write函数将这个数据包发出去。

d.私聊(`3`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号PRIVATE_CHAT,这是第四个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息““请输入您想聊天的客户端对象昵称以及聊天内容(用空格隔开):”,客户端输入自己想聊天的对象以及要发送的信息,如果这个聊天的对象在登录数组中没有找到,就说明目的客户端此时不在线或者是输入昵称有误;如果找到了就说明目的客户端此时在线。

随后就可以继续组建发送数据包了,  因为需要私聊,所以需要将目的客户端和服务器的连接套接字填充到这个发送数据包里面(填充的方法有多种:第一种是用一个字符变量来存储目的客户端和服务器的连接套接字(前提是这个套接字不是很大),这是一个整形,长字节赋值给短字节,遵循低字节拷贝高字节全部丢弃的原则;第二种是因为套接字是一个整形,有四个字节32位,可以将它分四次打包进要发送的数据包里面,>>不同位数之后,分别&上0xff,即可得到这个套接字某个字节上的原始八位数字,因为&上一个1原来的数值不会发生变化)然后就可以发送数据包给服务器了。

服务器收到数据包后会解析出接受者的套接字(这个方法也有多种: 第一种是用一个整型变量来存储刚刚客户端发来数据包的字符数;第二种对应上面填充的第二种方法,将分成的四个字符合成一个整形,需要<<恢复),解析完毕之后,服务器会出现提示这是客户端想发送给谁的信息?信息是什么?如果遍历完了登录数组都没有找到目的客户端,服务器就会提示服务器找不到目的客户端。

随后服务器也会组建一个发送的数据包,在数据包里面需要填充发送客户端和服务器的连接套接字(如何发送?请参考上面的两种方法)。

e.下线(`4`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号OFFLINE,这是第五个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“稍等......”,然后将数据包发送给服务器。服务器收到数据包后会在终端打印出“客户端想下线了...”,然后用for循环来找出是哪个用户想要下线(根据客户端和服务器的套接字在登录数组里面找到这个用户),然后再用一个for循环将想要下线的用户用后面的用户来填充,下线成功与否可以通过查看在线列表来验证。

f.群聊(`5`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号LOGIN,这是第六个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“欢迎进入多播聊天室!”。

这里的多播发送和多播接收要用到UDP协议,把这个用户加入多播组。先用socket函数创建一个套接字,指定参数为AF_INET(代表IPv4协议族)、SOCK_DGRAM(代表数据报套接字)、0(代表不知名的私有应用协议),在设置套接字的选项为可以加入多播组,也需要设置第四选项为可以重复利用端口号和IP地址,都用到了setsockopt函数,最后再绑定多播地址,用bind函数。再把最后的套接字传给下面的两个线程函数作为它们的参数。

客户端会创建两个线程,一个线程用来接收群聊信息,一个用来发送群聊信息(群聊跟服务器基本上没有什么关系)。

发送群聊信息:

在发送信息这里需要设置一个互斥锁、一个条件变量和一个共享变量,因为命令号的输入是在终端,群聊信息的输入也是在终端,会造成共享资源的抢占。

设置一个互斥锁来确保同一时间只有一个线程能访问缓冲区资源,避免共享资源的竞争。

设置一个条件变量和share来一起控制线程阻塞和唤醒。

设置一个共享变量share来控制线程的阻塞和唤醒,1阻塞0唤醒,share初始化为0。

在发送群聊信息这里先将共享变量share改为1,用时让send2函数sleep(1),这样就可以让send1函数先上锁,再用条件变量使线程send1阻塞,这里用了pthread_cond_wait函数(阻塞的条件是share!=0,在send1里面也需要sleep(1),这样就能够确保share能够先被send2函数改变为1),共享资源现在都是send2,send2用完之后唤醒条件变量,同时send2解锁。

接下来就可以在多播组之间群发信息了,加入了该多播组的用户都可以收到群发信息,用sendto函数发送信息。

客户端也会提示如何退出群聊,即输入第一个字符为`q`时,此时先唤醒条件变量,唤醒了条件变量之后,send1就不会堵塞了,然后send2解锁,这是为了防止带锁退出,那如何才能防止还能收到其他用户发送的多播信息呢?先用pthread_setcancelstate函数设置取消的状态为可以被别人取消,再用pthread_cancel函数来送一个取消请求给recv2函数。

接收群聊信息:

接收信息就很简单了,直接用recvfrom函数解析出多播信息来自于谁?信息是什么?

g.传文件(`6`):

客户端会组建发送数据包,这个数据包是字符数组类型的,该数据包的第一个字节需要填入对应的命令号SEND_FILE,这是第七个枚举常量/元素,它本身是一个int类型的,占四个字节,但是因为我们的数据包是字符数组类型的,会自动截短为一个字节,只取后面八位。

在终端输入这个命令号之后,客户端会出现提示信息“我想传输文件”,传输文件跟私聊的方法其实本质上是一样的。客户端先提示“请输入您想传输文件的客户端对象昵称以及文件名(用空格隔开):”, 根据昵称来判断该目的客户端是否在线。

和私聊唯一的区别就是传输文件需要打开要传输的文件并读取里面的内容,然后再将目的客户端和服务器的连接套接字以及文件的内容都写入到要发送的数据包里面(用sscanf函数实现)。

服务器收到数据包之后就会解析出接受者的连接套接字,方法如上私聊,服务器也需要组建一个发送数据包,将发送者和服务器的连接套接字以及文件内容填充进去(因为目的客户端需要知道是哪个用户发送的文件)。

接收服务器发送的信息:

a.查看在线列表(`2`):

客户端收到了服务器发送的数据包,根据该数据包第一个字节的命令号为ONLINE,可以得到在线列表。

再用memcpy函数将数据包第一个字节之后的内容复制到客户端登录数组里面,在对这个登录数组进行判断,大小为10,用for函数来判断有多少人在线,判断的条件是登录数组的connfd是否为0,若不为零说明该用户在线,若为零说明还不在线。

b.私聊(`3`):

目的客户端收到了服务器发送的数据包,根据该数据包第一个字节的命令号为PRIVATE_CHAT,可以先解析出发送客户端的套接字,如何解析?参考上面。

解析完成之后就可以得到这是来自谁的信息?信息是什么?

c.传文件(`6`):

目的客户端收到了来自服务器发送的数据包,根据该数据包第一个字节的命令号为SEND_FILE,可以先解析出发送者的连接套接字,如何解析?参考上面。

解析之后就可以知道这是哪个用户发送的文件,以“w+”(读写方式打开,若文件不存在,则创建,打开后文件的内容截短)方式打开一个文件,然后将接收的内容写入到这个打开的文件里面,传输文件成功。


项目效果:

服务器收到客户端连接请求

d65ce88bf6de4f0693445a94b87c4a96.png

注册(若已经注册过服务器会提示用户名已存在) 868fc2f81a654eda951eff9fc93c45be.png

登录(昵称和密码错误服务器都会做出相应提示) 0f509c6d4cd64fcc8adb59de82d2b6d1.png

 查看在线列表172f351903f1457ab63161e83acdb530.png

e3955a7d946e498b996f02f0b71e8c39.png

 私聊

ef7d3270583442798bbf0cfbec016bf6.png

 (若目的客户端不在线或输入昵称有误则提示)

867cb12f228643a98bc8a7a8efea34dc.png

传文件 b3dad89bb4fd44a2b51d57263ea9b62c.png

 afa556f245dd42ab8fc42693c99cbd4a.png

 群聊1fe97f8f56c44b37b1c73679a3d4b669.png

 413292978aa64ab0aef7208919d96927.png

82b78ee0a42043c1ba5e130873d4af65.png 下线


bedf1c1636c444e194ed4af5cc44e30c.png


源码:

chatroom_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

struct sign_user{
    char name[64];
    char password[100];
    int connfd;
};

struct login_user{
    char name[64];
    char password[100];
    int connfd;
};

struct sign_user sign[10];
struct login_user login[10];

int si = 0;
int li = 0;

enum CMD{
    REGISTER, LOGIN, ONLINE, PRIVATE_CHAT, OFFLINE, GROUP_CHAT, SEND_FILE
};

pthread_mutex_t mutex;//设置一个互斥锁来确保同一时间只有一个线程能访问缓冲区资源,避免数据竞争
pthread_cond_t cond;//设置一个条件变量和share一起来控制线程阻塞和唤醒
int share = 0;//设置一个共享变量来控制线程阻塞和唤醒(1阻塞0唤醒)

pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;

/*
    功能:初始客户端
    参数:
        @ip:点分式网络地址
        @port:端口号
*/
int client_init(const char *ip, short port)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        perror("socket error");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);//把一个16bits的主机字节序(小端字节序)转换为网络字节序(大端字节序)
    saddr.sin_addr.s_addr = inet_addr(ip);//把点分式字符串网络地址转换成二进制网络地址

    if(connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) == -1)
    {
        perror("connect error");
        return -1;
    }

    return sockfd;
}

/*
    功能:创建一个线程函数来接收来自服务器的信息(私聊)
    参数:
        @arg:传入的参数
*/
void *recv1_msg(void *arg)
{
    int sockfd = *((int *)arg);
    
    while(1)
    {
        char recvbuf[1024] = {0};
        int ret = read(sockfd, recvbuf, 1000);
        if(ret == 0)
        {
            printf("服务器异常\n");
            break;
        }
        else if(ret > 0)
        {
            if(recvbuf[0] == ONLINE)//服务器发了在线列表
            {
                printf("在线列表如下:\n");
                memcpy(login, recvbuf+1, sizeof(login));
                for(int i=0; i<10; ++i)
                {
                    if(login[i].connfd != 0)//在线
                    {
                        printf("%s\t", login[i].name);
                    }
                }
                printf("\n");
            }
            else if(recvbuf[0] == PRIVATE_CHAT)
            {
                printf("----聊天啦----\n");
                //解析出发送者的套接字
                int sendfd = recvbuf[1];
                // char s = recvbuf[1];
                // printf("s = %d\n", s);
                // int sendfd = (int)s;
                // printf("sendfd = %d\n", sendfd);
                // printf("recvfd = %d\n", recvbuf[1]);
                
                // int sendfd = recvbuf[1] << 0 | recvbuf[2] << 8 | recvbuf[3] << 16 | recvbuf[4] << 24;
                // printf("sendfd = %d\n", sendfd);
                // printf("connfd = %d\n", login[0].connfd);
                // printf("connfd = %d\n", login[1].connfd);
                int i;
                for(i=0; i<10; ++i)
                {
                    // printf("i = %d\n", i);
                    // printf("%d\n", login[i].connfd);
                    // printf("%d\n", sendfd);
                    if(login[i].connfd == sendfd)
                    {
                        printf("这是来自%s用户的信息:", login[i].name);
                        // printf("%s\n", recvbuf+5);
                        printf("%s\n", recvbuf+2);
                        break;
                    }
                }
                if(i == 10)
                {
                    // printf("来自未知发件人发送的:%s\n", recvbuf+5);
                    printf("来自未知发件人发送的:%s\n", recvbuf+2);
                }
            }
            else if(recvbuf[0] == SEND_FILE)
            {
                printf("----收文件啦----\n");
                //解析出发送者的套接字
                int sendfd = recvbuf[1] << 0 | recvbuf[2] << 8 | recvbuf[3] << 16 | recvbuf[4] << 24;
                int i;
                for(i=0; i<10; ++i)
                {
                    if(login[i].connfd == sendfd)
                    {
                        printf("这是来自%s用户的文件:", login[i].name);
                        printf("%s\n", recvbuf+5);
                        char s[1024] = {0};
                        strcpy(s, recvbuf+5);
                        printf("s: %s\n", s);
                        char l[100] = "cp.txt";
                        FILE *fp = fopen(l, "w+");
                        if(fp == NULL)
                        {
                            perror("fopen error");
                            return NULL;
                        }
                        int size = fwrite(s, 1, strlen(s), fp);
                        if(size == -1)
                        {
                            perror("fwrite error");
                            return NULL;
                        }
                        fclose(fp);
                        break;
                    }
                }
                if(i == 10)
                {
                    printf("来自未知发件人发送的\n");
                    printf("%s\n", recvbuf+5);
                    char s[1024] = {0};
                    strcpy(s, recvbuf+5);
                    char l[100] = "cp.txt";
                    FILE *fp = fopen(l, "w+");
                    if(fp == NULL)
                    {
                        perror("fopen error");
                        return NULL;
                    }
                    int size = fwrite(s, 1, strlen(s), fp);
                    if(size == -1)
                    {
                        perror("fwrite error");
                        return NULL;
                    }
                    fclose(fp);
                }
            }
        }
        else 
        {
            perror("read error");
            return NULL;
        }
    }
}

/*
    功能:功能菜单
    参数:
        无
*/
void menu()
{
    printf("---------------功能查询表------------------\n");
    printf("---------------0: 注册---------------------\n");
    printf("---------------1: 登录/上线----------------\n");
    printf("---------------2: 查看在线列表-------------\n");
    printf("---------------3: 私聊---------------------\n");
    printf("---------------4: 下线---------------------\n");
    printf("---------------5: 群聊---------------------\n");
    printf("---------------6: 传输文件-----------------\n");
}

/*
    功能:创建一个线程函数来实现多播发送
    参数:
        @arg: 传入的参数
*/
void *send2_msg(void *arg)
{
    int multifd = *((int *)arg);

    while(1)
    {
        char buf[1024] = {0};
        share = 1;//共享变量设置为1,可以让send1先上锁,再用条件变量使线程send1阻塞,共享资源现在都是send2的,send2用完之后唤醒条件变量,同时send2解锁
        sleep(1);//确保send1线程抢到共享资源
        pthread_mutex_lock(&mutex);
        // printf("send2上锁:%d\n", pthread_mutex_lock(&mutex));
        printf("第一个字符输入q--->退出群聊!\n");
        printf("\n");
        fgets(buf, 1000, stdin);
        if(buf[0] == 'q')
        {
            share = 0;
            if(pthread_cond_signal(&cond) != 0)
            {
                perror("pthread_cond_signal error");
                return NULL;
            }
            pthread_mutex_unlock(&mutex);
            // printf("send2解锁:%d\n", pthread_mutex_unlock(&mutex));//防止带锁退出
            if(pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL) != 0)
            {
                perror("pthread_setcancelstate error");
                return NULL;
            }
            int r = pthread_cancel(tid4);//用来发送一个“取消请求”给recv2线程
            if (r != 0)
            {
                perror("pthread_cancel error");
                return NULL;
            }
            pthread_exit((void *)1);
        }

        struct sockaddr_in *saddr = malloc(sizeof(struct sockaddr_in));
        saddr->sin_family = AF_INET;
        saddr->sin_port = htons(54321);
        saddr->sin_addr.s_addr = inet_addr("224.10.10.1"); //多播地址字符串
        socklen_t len = sizeof(*saddr);
        int size = sendto(multifd, buf, strlen(buf)+1, 0, (struct sockaddr *)saddr, len);
        // printf("size:%d\n",size);
        // printf("%s发送多播信息\n", inet_ntoa(saddr.sin_addr));
        printf("发送的多播信息是:%s\n", buf);
        pthread_mutex_unlock(&mutex);
        // printf("send2解锁:%d\n", pthread_mutex_unlock(&mutex));
    }
}

/*
    功能:创建一个线程函数来实现多播接收
    参数:
        @arg: 传入的参数
*/
void *recv2_msg(void *arg)
{
    int multifd = *((int *)arg);
    // printf("multifd = %d\n", multifd);
    while(1)
    {
        char buf[1024] = {0};
        struct sockaddr_in saddr;
        socklen_t len = sizeof(saddr);
        recvfrom(multifd, buf, 1000, 0, (struct sockaddr*)&saddr, &len);
        printf("多播信息来自于%s\n", inet_ntoa(saddr.sin_addr));
        printf("信息是:%s\n", buf);
    }
}


/*
    功能:加入多播组
    参数:
        无
*/
int join_multi()
{
    //1.创建一个套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd == -1)
    {
        perror("socket error");
        return -1;
    }
    // printf("sockfd = %d\n", sockfd);

    //2.设置套接字选项-->加入多播组
    struct ip_mreq mreq;
    mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1"); //D类地址 例:224.20.20.1	
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);//让内核随便帮我们选一个
    if(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1)
    {
        perror("set IP_ADD_MEMBERSHIP error");
        return -1;
    }

    int on;
    if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) == -1)
    {
        perror("set SO_REUSEPORT error");
        return -1;
    }

    if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
    {
        perror("set SO_REUSEADDR error");
        return -1;
    }
    //3.绑定多播地址
    struct sockaddr_in maddr;
    maddr.sin_family = AF_INET;
    maddr.sin_port = htons(54321);
    maddr.sin_addr.s_addr = inet_addr("224.10.10.1"); //多播地址字符串
    if(bind(sockfd, (struct sockaddr *)&maddr, sizeof(maddr)) == -1)
    {
        perror("bind error");
        return -1;
    }
    
    // printf("sockfd1 = %d\n", sockfd);
    return sockfd;
}

/*
    功能:创建一个线程函数来将客户端的信息发送给服务器(私聊)
    参数:
        @arg:传入的参数
*/
void *send1_msg(void *arg)
{
    int sockfd = *((int *)arg);

    while(1)
    {
        menu();
        pthread_mutex_lock(&mutex);
        // printf("send1上锁:%d\n", pthread_mutex_lock(&mutex));
        sleep(1);//确保share能先被send2变为1
        // printf("share = %d\n", share);
        if(share != 0)
        {
            // printf("睡告告\n");
            if(pthread_cond_wait(&cond, &mutex) != 0)
            {
                perror("pthread_cond_wait");
                return NULL;
            }
            // printf("我醒了\n");
        }
        char cmd = getchar();
        getchar();
        pthread_mutex_unlock(&mutex);
        // printf("send1解锁:%d\n", pthread_mutex_unlock(&mutex));
    
        if(cmd == '0')//注册
        {
            printf("请输入您的昵称(以#结束):");
            char sendbuf[64] = {0};
            sendbuf[0] = REGISTER;
            gets(sendbuf+1);
            puts(sendbuf+1);
            int l= strlen(sendbuf+1);
            printf("请输入您的密码:");
            gets(sendbuf+1+l);
            puts(sendbuf+1+l);
            write(sockfd, sendbuf, strlen(sendbuf+1)+2);
        }
        else if(cmd == '1')//登录
        {
            char sendbuf[100] = {0};
            char sendbuf1[64] = {0};
            char sendbuf2[64] = {0};
            printf("请输入您的昵称(以#结束)和密码:");
            scanf("%s", sendbuf+1);
            getchar();
            sendbuf[0] = LOGIN;
            write(sockfd, sendbuf, strlen(sendbuf+1)+2);
        }
        else if(cmd == '2')//查看在线列表
        {
            printf("稍等......\n");
            char sendbuf[64] = {0};
            sendbuf[0] = ONLINE;
            write(sockfd, sendbuf, 1);
        }
        else if(cmd == '3')//私聊
        {
            printf("我想聊天了\n");
            char buf[1024] = {0};
            char name[64] = {0};
            char msg[1000] = {0};
            printf("请输入您想聊天的客户端对象昵称以及聊天内容(用空格隔开):\n");
            fgets(buf, 1024, stdin);
            sscanf(buf, "%s %s", name, msg);

            //组建发送数据包
            char sendbuf[1024] = {0};
            sendbuf[0] = PRIVATE_CHAT;
            //根据目的客户端昵称,在在线列表中找到这个对象和服务器的连接套接字
            int i = 0;
            for(i=0; i<10; ++i)
            {
                if(strcmp(name, login[i].name) == 0)
                {
                    printf("目的客户端此时在线\n");
                    break;
                }
            }
            if(i == 10)
            {
                printf("目的客户端此时不在线或输入昵称有误!\n");
                continue;
            }


            char s = login[i].connfd;//目的客户端和服务器的连接套接字
            // printf("s = %d\n", s);
            // printf("connfd = %d\n", login[i].connfd);
            sendbuf[1] = s;
            strcpy(sendbuf+2, msg);
            write(sockfd, sendbuf, strlen(sendbuf+2)+3);

            // sendbuf[1] = (login[i].connfd) & 0xff;
            // sendbuf[2] = (login[i].connfd >> 8) & 0xff;
            // sendbuf[3] = (login[i].connfd >> 16) & 0xff;
            // sendbuf[4] = (login[i].connfd >> 24) & 0xff;
            // strcpy(sendbuf+5, msg);
            // write(sockfd, sendbuf, strlen(sendbuf+5)+6);
            
        }
        else if(cmd == '4')//下线
        {
            printf("稍等......\n");
            char sendbuf[64] = {0};
            sendbuf[0] = OFFLINE;
            write(sockfd, sendbuf, 1);
        }
        else if(cmd == '5')//群聊
        {
            char sendbuf[64] = {0};
            sendbuf[0] = GROUP_CHAT;
            write(sockfd, sendbuf, 1);
            printf("欢迎进入多播聊天室!\n");
            int *multifd = malloc(4);
            *multifd = join_multi(); 
            // printf("*multifd = %d\n", *multifd);

            if(pthread_create(&tid3, NULL, send2_msg, (void *)multifd) != 0)
            {
                perror("pthread_create error");
                return NULL;
            }

            if(pthread_create(&tid4, NULL, recv2_msg, (void *)multifd) != 0)
            {
                perror("pthread_create error");
                return NULL;
            }
        }
        else if(cmd == '6')//传输文件
        {
            char sendbuf[1024] = {0};
            sendbuf[0] = SEND_FILE;
            printf("我想传输文件\n");
            char buf[1024] = {0};
            char name[64] = {0};
            char filename[64] = {0};
            printf("请输入您想传输文件的客户端对象昵称以及文件名(用空格隔开):\n");
            fgets(buf, 1024, stdin);
            sscanf(buf, "%s %s", name, filename);

            //根据目的客户端昵称,在在线列表中找到这个对象和服务器的连接套接字
            int i = 0;
            for(i=0; i<10; ++i)
            {
                if(strcmp(name, login[i].name) == 0)
                {
                    printf("目的客户端此时在线\n");
                    break;
                }
            }
            if(i == 10)
            {
                printf("目的客户端此时不在线或输入昵称有误!\n");
                continue;
            }

            //打开要传输的文件
            FILE *fp = fopen(filename, "r+");
            if(fp == NULL)
            {
                perror("fopen error");
                return NULL;
            }

            //读取已经打开的文件
            char s[1024] = {0};
            int size = fread(s, 1, 1000, fp);
            if(size == -1)
            {
                perror("fread error");
                return NULL;
            }

            sendbuf[1] = (login[i].connfd) & 0xff;
            sendbuf[2] = (login[i].connfd >> 8) & 0xff;
            sendbuf[3] = (login[i].connfd >> 16) & 0xff;
            sendbuf[4] = (login[i].connfd >> 24) & 0xff;

            strcpy(sendbuf+5, s);
            write(sockfd, sendbuf, strlen(sendbuf+5)+6);
        }
    }
}

/*
    功能:封装私聊相关函数
    参数:
        @ip:点分式网络地址
        @port:端口号
        
*/
void private_chat(const char *ip, short port)
{
    int sockfd = client_init(ip, port);
    if(sockfd == -1)
    {
        return ;
    }

    if(pthread_create(&tid1, NULL, recv1_msg, (void *)&sockfd) != 0)
    {
        perror("pthread_create error");
        return ;
    }
   
    if(pthread_create(&tid2, NULL, send1_msg, (void *)&sockfd) != 0)
    {
        perror("pthread_create error");
        return ;
    }
   
}

int main(int argc, const char *argv[])
{
    if(argc != 3)
    {
        printf("arg error");
        return -1;
    }

    if(pthread_mutex_init(&mutex, NULL) != 0)
    {
        perror("pthread_mutex_init error");
        return -1;
    }

    if(pthread_cond_init(&cond, NULL) != 0)
    {
        perror("pthread_cond_init error");
        return -1;
    }

    private_chat(argv[1], atoi(argv[2]));

    while(1);
}

chatroom_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

struct sign_user{
    char name[64];
    char password[100];
    int connfd;
};

struct login_user{
    char name[64];
    char password[100];
    int connfd;
};

struct sign_user sign[10];
struct login_user login[10];

int si = 0;
int li = 0;

enum CMD{
    REGISTER, LOGIN, ONLINE, PRIVATE_CHAT, OFFLINE, GROUP_CHAT, SEND_FILE
};

/*
    功能:创建一个线程函数来接收来自客户端的信息(解析出相应操作并发给对应的客户端)
    参数:
        @arg:传入的参数
*/
void *handle_connection(void *arg)
{
    int connfd = *((int *)arg);

    while(1)
    {
        char recvbuf[1024] = {0};
        int ret = read(connfd, recvbuf, 1000);
        // printf("recvbuf[0] = %d\n", recvbuf[0]);
        if(ret == 0)
        {
            printf("客户端断开连接了...\n");
            break;
        }
        else if(ret > 0)
        {
            if(recvbuf[0] == REGISTER)//注册
            {
                printf("用户%s注册成功!\n", sign[si].name);
                printf("\n");
                //用#来分隔昵称和密码
                int j = 1;
                while(1)
                {
                    if(recvbuf[j] == '#')
                    {
                        break;
                    }
                    j++;
                }
                strncpy(sign[si].name, recvbuf+1, j);
                int flag = 0;//用来判断是否重命名(1是0否)
                for(int i=si; i>0; --i)
                {
                    if(strcmp(sign[si].name, sign[i-1].name) == 0)
                    {
                        printf("该用户名已存在,请重新注册!\n");
                        printf("\n");
                        strcpy(sign[si].name, "");
                        flag = 1;
                        break;
                    }
                }
                if(flag == 1)
                {
                    continue;
                }
                sign[si].connfd = connfd;
                strcpy(sign[si].password, recvbuf+1+j);
                si++;
            }
            else if(recvbuf[0] == LOGIN)//登录
            {
                //用#来分隔昵称和密码
                int l = 1;
                while(1)
                {
                    if(recvbuf[l] == '#')
                    {
                        break;
                    }
                    l++;
                }
                int j;
                printf(".....正在查询有无此人.....\n");
                for(j=0; j<10; ++j)
                { 
                    if(strncmp(recvbuf+1, sign[j].name, l) != 0)
                        continue;
                    else
                    {
                        printf("输入昵称正确!\n");
                        printf("\n");
                        break;
                    }  
                }
                if(j == 10)
                {
                    printf("输入昵称错误,您还没有注册,请先注册再登录!\n");
                    printf("\n");
                    continue;
                }
                else
                {
                    printf(".....正在查询密码是否正确.....\n");
                    if(strcmp(recvbuf+1+l, sign[j].password) != 0)
                    {
                        printf("密码错误,请重新输入!\n");
                        printf("\n");
                    }   
                    else
                    {
                        int flag = 0;//用来判断是否重命名(1是0否)
                        strncpy(login[li].name, recvbuf+1, l);
                        for(int i=li; i>0; --i)
                        {
                            if(strcmp(login[li].name, login[i-1].name) == 0)
                            {
                                printf("用户%s已经登录过了!\n", login[li].name);
                                printf("\n");
                                flag = 1;
                                break;
                            }
                        }
                        if(flag == 1)
                        {
                            continue;
                        }
                        printf("用户%s登录成功!\n", login[li].name);
                        printf("\n");
                        printf("欢迎%s上线\n", login[li].name);
                        printf("\n");
                        login[li].connfd = connfd;
                        strcpy(login[li].password, recvbuf+1+l);
                        li++;
                    }
                }
            }
            else if(recvbuf[0] == ONLINE)//客户端请求查看在线列表
            {
                printf("客户端请求查看在线列表...\n");
                printf("\n");
                char sendbuf[1000] = {0};
                sendbuf[0] = ONLINE;
                memcpy(sendbuf+1, login, sizeof(login));
                write(connfd, sendbuf, sizeof(login)+1+1);
            }
            else if(recvbuf[0] == PRIVATE_CHAT)//聊天
            {
                printf("有客户端想聊天了...\n");
                printf("\n");
                //解析出接受者的套接字
                int recvfd = recvbuf[1];
                // char s = recvbuf[1];
                // int recvfd = (int)s;
                // printf("recvfd = %d\n", recvfd);

                // int recvfd = recvbuf[1] | recvbuf[2] << 8 | recvbuf[3] << 16 | recvbuf[4] << 24;
                int i = 0;
                for(i=0; i<10; ++i)
                {
                    if(login[i].connfd == recvfd)
                    {
                        // printf("connfd = %d\n", login[i].connfd);
                        // printf("name = %s\n", login[i].name);
                        printf("客户端想发送信息给%s\n", login[i].name);
                        printf("信息是:%s\n", recvbuf+2);
                        // printf("name = %s\n", login[i].name);
                        printf("\n");
                        break;
                    }
                }
                if(i == 10)
                {
                    printf("服务器找不到目的客户端!\n");
                    printf("\n");
                    continue;
                }

                char sendbuf[1024] = {0};
                sendbuf[0] = PRIVATE_CHAT;
                sendbuf[1] = connfd;//发送者的套接字
                // sendbuf[1] = connfd & 0xff;
                // sendbuf[2] = (connfd >> 8) & 0xff;
                // sendbuf[3] = (connfd >> 16) & 0xff;
                // sendbuf[4] = (connfd >> 24) & 0xff;
                strncpy(sendbuf+2, recvbuf+2, strlen(recvbuf+2));
                write(recvfd, sendbuf, strlen(sendbuf+2)+3);
                // strncpy(sendbuf+5, recvbuf+5, strlen(recvbuf+5));
                // write(recvfd, sendbuf, strlen(sendbuf+5)+6);
            }
            else if(recvbuf[0] == OFFLINE)
            {
                printf("客户端想下线了...\n");
                printf("\n");

                int i;
                for(i=0; i<10; ++i)
                {
                    if(login[i].connfd == connfd)
                    {
                        break;
                    }
                }
                // printf("%d\n", login[i].connfd);
                printf("下线客户端为:%s\n", login[i].name);
                int j;
                for(j=i; j<10; ++j)
                {
                    login[j] = login[j+1];
                }
                
                printf("客户端下线成功!\n");
                printf("\n");
            }
            else if(recvbuf[0] == GROUP_CHAT)
            {
                printf("进入多播聊天室!\n");
                printf("\n");
            }
            else if(recvbuf[0] == SEND_FILE)
            {
                printf("有客户端想传输文件!\n");
                //解析出接受者的套接字
                int recvfd = recvbuf[1] | recvbuf[2] << 8 | recvbuf[3] << 16 | recvbuf[4] << 24;
                int i = 0;
                for(i=0; i<10; ++i)
                {
                    if(login[i].connfd == recvfd)
                    {
                        printf("客户端想发文件给%s\n", login[i].name);
                        printf("\n");
                        break;
                    }
                }
                if(i == 10)
                {
                    printf("服务器找不到目的客户端!\n");
                    printf("\n");
                    continue;
                }

                char sendbuf[1024] = {0};
                sendbuf[0] = SEND_FILE;
                sendbuf[1] = connfd & 0xff;
                sendbuf[2] = (connfd >> 8) & 0xff;
                sendbuf[3] = (connfd >> 16) & 0xff;
                sendbuf[4] = (connfd >> 24) & 0xff;
                strncpy(sendbuf+5, recvbuf+5, strlen(recvbuf+5));
                write(recvfd, sendbuf, strlen(sendbuf+5)+6);
            }
        }
        else
        {
            perror("read error");
            return NULL;
        }
    }
}

/*
    功能:初始化服务器
    参数:
        @ip:点分式网络地址
        @port:端口号
*/
int server_init(char *ip, short port)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        perror("socket error");
        return -1;
    }
    
    // int on;
    // if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) == -1)
    // {
    //     perror("set SO_REUSEPORT error");
    //     return -1;
    // }

    // if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
    // {
    //     perror("set SO_REUSEADDR error");
    //     return -1;
    // }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(port);//把一个16bits的主机字节序(小端字节序)转换为网络字节序(大端字节序)
    saddr.sin_addr.s_addr = inet_addr(ip);//把点分式字符串网络地址转换成二进制网络地址
    if(bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) == -1)
    {
        perror("bind error");
        return -1;
    }

    if(listen(sockfd, 10) == -1)
    {
        perror("listen error");
        return -1;
    }

    while(1)
    {
        struct sockaddr_in maddr;
        socklen_t len = sizeof(maddr);
        int *connfd = malloc(4); 
        *connfd = accept(sockfd, (struct sockaddr*)&maddr, &len);
        if(*connfd == -1)
        {
            printf("accept error");
            return -1;
        }
        else//客户端连接成功
        {
            printf("服务器收到来自客户端%s[%d]的连接请求\n", inet_ntoa(maddr.sin_addr), ntohs(maddr.sin_port));
            pthread_t tid;
            if(pthread_create(&tid, NULL, handle_connection, (void *)connfd) != 0)
            {
                perror("pthread_create error");
                return -1;
            }
        }
    }   

    return sockfd;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        printf("arg error");
        return -1;
    }

    int sockfd = server_init(argv[1], atoi(argv[2]));
    if(sockfd == -1)
    {
        return -1;
    }

    close(sockfd);
}

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐