本项目主要完成多线程并发聊天室的基础功能,即多个客户端之间通过服务器可以实现群发消息,重点在于学习网络编程,多线程和线程同步的基础知识(基于Linux)。
下面我会详解每一部分的代码。
1.主线程
1.1首先由于是自己在电脑里面测试,所以可以自己开线程去验证,所以main函数如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <semaphore.h>
sem_t semid;//信号量
char name[64] = "[DEFAULT]";//姓名,默认default
#define MAX_CLIENT 100 //最大可以连接的客户端数量
int client_socks[MAX_CLIENT]; //保存100个客户端的套接字信息
pthread_mutex_t mutex;//互斥量,线程同步
//上面为一些变量的定义和初始化
int main(int argc, char* argv[])
{
memset(client_socks, -1, sizeof(client_socks));//初始化全为无效套接字
invoke(argv[1]);//传入命令行的输入参数
return 0;
}
1.2 main函数接收来自控制台的命令,然后开始执行Invoke函数来开启服务端和客户端,代码如下
void invoke(const char * arg)
{
if (strcmp(arg, "s") == 0) {//检查输入的命令行的第二个参数,如果为s,表示开启服务端,否则开启客户端
Server();
}
else {//其他任意字母
Client();
}
}
2.服务端
2.1输入控制台的命令带s表示启动服务端,否则启动客户端,服务端要先启动,代码如下
void Server()
{
//初始化
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
server_socket = socket(PF_INET, SOCK_STREAM, 0);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//在所有IP上监听
server_addr.sin_family = AF_INET;//
server_addr.sin_port = htons(9000);//端口
pthread_mutex_init(&mutex, NULL);//初始化互斥量
if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == -1){
printf("bind error %d");
close(server_socket);
return;
}
if (listen(server_socket, 5) == -1) {
printf("listen error\n");
close(server_socket);
return;
}
while (1) {
socklen_t client_addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
if (client_socket == -1) {
printf("accept error\n");
}
//将客户端处理交个线程
pthread_mutex_lock(&mutex);//上锁
int i = 0;
for (; i < MAX_CLIENT; i++)//寻找空闲的数组位置,来存储客户端套接字
{
if (client_socks[i] == -1 ) {//找到空闲位置
client_socks[i] = client_socket;//将得到的客户端套接字保存到客户端数套接字数组
break;
}
}
pthread_mutex_unlock(&mutex);//解锁
pthread_t thread_id;//声明线程ID
pthread_create(&thread_id, NULL, handle_client, &client_socks[i]);//创建线程 ,取到这个客户端套接字传给新线程去处理
}
close(server_socket);
pthread_mutex_destroy(&mutex);//销毁互斥量
}
2.2服务端主要设计网络套接字的初始化,然后循环接收来自客户端的连接,并开启一个线程去保持和客户端连接,保证主线程不阻塞,即实现了多线程,提高连接效率,减少客户端的等待时间。开启线程函数设计的函数指针是handle_client,即客户端处理函数,该函数代码如下。
void* handle_client(void* arg)//处理客户端消息
{
pthread_detach(pthread_self());
int client_sock = *(int*)arg;//解析套接字
char msg[1024] = "";
ssize_t str_len = 0;
while ((str_len = read(client_sock, msg, sizeof(msg))) > 0) {//整个表达式就是read读到的字节数
sendMessage(msg, str_len);
}
pthread_mutex_lock(&mutex);//上锁
*(int*)arg = -1;//arg为对应套接字的地址,设置套接字
pthread_mutex_lock(&mutex);//解锁
close(client_sock);
pthread_exit(NULL);
}
2.3该函数循环和客户端保持连接,一旦收到客户端的消息就可以群发给其他在线客户端,服务段发送消息给所以客户端sendMessage函数如下
void sendMessage(const char* msg, ssize_t str_len)//转发消息给所有的客户端
{
pthread_mutex_lock(&mutex);//上锁
for (int i = 0; i < MAX_CLIENT; i++)
{
if (client_socks[i] !=-1) {//还没给客户端发过消息 我才发送
write(client_socks[i], msg, str_len);//发送数据
}
else {
break;//后面的一定无效,因为每次都往数从前往后存放客户端套接字到client_socks
}
}
pthread_mutex_unlock(&mutex);//解锁
}
3.客户端
3.1服务器至此结束,下面来看客户端,首先是客户端的初始化代码,每连接一个客户端,我们需要先告诉客户端姓名,然后连接成功后就可以发送消息给服务端了。
void Client()
{
int client_socket;
struct sockaddr_in server_addr;
client_socket = socket(PF_INET, SOCK_STREAM, 0);
//初始化
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_family = AF_INET;//
server_addr.sin_port = htons(9000);//端口
printf("[ input name ]: ");
scanf("%s", name);
fgetc(stdin);//读取最后一个\n,防止下次调用fgets 的时候读到换行符
printf("connect success\n");
if (connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr))==-1) {
printf("connect error\n");
close(client_socket);
return;
}
pthread_t send_id, recv_id;
sem_init(&semid, 0, -1);//初始化信号量为-1
//创建两个线程分别去处理发送群消息和接收群消息
pthread_create(&send_id, NULL, client_send_message, (void*)&client_socket);// 此处传的时局部变量的地址,必须保证子线程结束后,该线程才能结束
pthread_create(&recv_id, NULL, client_recv_message, (void*)&client_socket);//所以下面使用了信号量来等到两个子线程的结束
sem_wait(&semid);//等待上面两个线程结束,这里用信号量,初始信号量设为-1,上述2线程个执行一次sem_post,信号量变为1,此处就有信号了
close(client_socket);
}
3.2 上面的两个线程分别去处理发送消息和接收消息,代码如下
void* client_send_message(void* arg)
{
pthread_detach(pthread_self());//搭配pthread_exit使用,线程结束后自动销毁
int client_socket = *(int*)arg;
char msg[1000] = "";//输入的消息
char send_buffer[1000] = "";//要发送的消息
while (true) {
memset(send_buffer, 0, sizeof(send_buffer));
printf("input message to send everyone:\n");
fgets(msg, sizeof(msg), stdin);//从输入流里面得到消息存取在msg中
if ((strcmp(msg, "q\n") == 0) || (strcmp(msg, "Q\n") == 0)) {//输入q/Q退出消息
printf("successful exit\n ");
break;
}
snprintf(send_buffer, sizeof(send_buffer), "=========== [%s say]: %s", name, msg);//拼接 姓名+消息 格式化输出到 send_buffer中
write(client_socket, send_buffer, strlen(send_buffer));//发送给服务器
usleep(100);//延时0.1ms
}
close(client_socket);
sem_post(&semid);//信号量+1
pthread_exit(NULL);
}
3.3下面是接收来自服务端的代码,如下
void* client_recv_message(void* arg)
{
pthread_detach(pthread_self());//自动销毁线程函数,pthread_self为当前线程id
int client_socket = *(int*)arg;
char msg[512] = "";
while (1) {
ssize_t str_len = read(client_socket, msg,sizeof(msg));
if (str_len <= 0) {
break;
}
fputs(msg, stdout);//输出
memset(msg, 0, strlen(msg));
}
close(client_socket);
sem_post(&semid);//信号量+1
pthread_exit(NULL);
}
4.总结
主要实现了群聊的底层功能,再此基础上,我们还可以实现给某个客户端单独发消息,只需让服务器发送消息的时候找到要发送的套接字,然后只单独发送给客户端。这里先不实现了,我感觉在来点图像化界面,多增加一些功能,类似小型交友软件就有了。
5.输出
首先启动客户端,编译好后输入指令运行,记得在指令后面带上参数s 表示启动服务端
其次启动服务器,找到编译好的文件,后面随便带一个非s的字母,就可启动客户端,可以启动多个客户端,这里以两个为例。
6.整体代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <semaphore.h>
sem_t semid;//信号量
char name[64] = "[DEFAULT]";//姓名,默认default
#define MAX_CLIENT 100 //最大可以连接的客户端数量
int client_socks[MAX_CLIENT]; //保存100个客户端的套接字信息
pthread_mutex_t mutex;//互斥量,线程同步
void sendMessage(const char* msg, ssize_t str_len)//转发消息给所有的客户端
{
pthread_mutex_lock(&mutex);//上锁
for (int i = 0; i < MAX_CLIENT; i++)
{
if (client_socks[i] !=-1) {//还没给客户端发过消息 我才发送
write(client_socks[i], msg, str_len);//发送数据
}
else {
break;//后面的一定无效,因为每次都往数从前往后存放客户端套接字到client_socks
}
}
pthread_mutex_unlock(&mutex);//解锁
}
void* handle_client(void* arg)//处理客户端消息
{
pthread_detach(pthread_self());
int client_sock = *(int*)arg;//解析套接字
char msg[1024] = "";
ssize_t str_len = 0;
while ((str_len = read(client_sock, msg, sizeof(msg))) > 0) {//整个表达式就是read读到的字节数
sendMessage(msg, str_len);
}
pthread_mutex_lock(&mutex);//上锁
*(int*)arg = -1;//arg为对应套接字的地址,设置套接字
pthread_mutex_lock(&mutex);//解锁
close(client_sock);
pthread_exit(NULL);
}
void Server()
{
//初始化
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
server_socket = socket(PF_INET, SOCK_STREAM, 0);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//在所有IP上监听
server_addr.sin_family = AF_INET;//
server_addr.sin_port = htons(9000);//端口
pthread_mutex_init(&mutex, NULL);//初始化互斥量
if (bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)) == -1){
printf("bind error %d");
close(server_socket);
return;
}
if (listen(server_socket, 5) == -1) {
printf("listen error\n");
close(server_socket);
return;
}
while (1) {
socklen_t client_addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_size);
if (client_socket == -1) {
printf("accept error\n");
}
//将客户端处理交个线程
pthread_mutex_lock(&mutex);//上锁
int i = 0;
for (; i < MAX_CLIENT; i++)//寻找空闲的数组位置,来存储客户端套接字
{
if (client_socks[i] == -1 ) {//找到空闲位置
client_socks[i] = client_socket;//将得到的客户端套接字保存到客户端数套接字数组
break;
}
}
pthread_mutex_unlock(&mutex);//解锁
pthread_t thread_id;//声明线程ID
pthread_create(&thread_id, NULL, handle_client, &client_socks[i]);//创建线程 ,取到这个客户端套接字传给新线程去处理
}
close(server_socket);
pthread_mutex_destroy(&mutex);//销毁互斥量
}
void* client_send_message(void* arg)
{
pthread_detach(pthread_self());//搭配pthread_exit使用,线程结束后自动销毁
int client_socket = *(int*)arg;
char msg[1000] = "";//输入的消息
char send_buffer[1000] = "";//要发送的消息
while (true) {
memset(send_buffer, 0, sizeof(send_buffer));
printf("input message to send everyone:\n");
fgets(msg, sizeof(msg), stdin);//从输入流里面得到消息存取在msg中
if ((strcmp(msg, "q\n") == 0) || (strcmp(msg, "Q\n") == 0)) {//输入q/Q退出消息
printf("successful exit\n ");
break;
}
snprintf(send_buffer, sizeof(send_buffer), "=========== [%s say]: %s", name, msg);//拼接 姓名+消息 格式化输出到 send_buffer中
write(client_socket, send_buffer, strlen(send_buffer));//发送给服务器
usleep(100);//延时0.1ms
}
close(client_socket);
sem_post(&semid);//信号量+1
pthread_exit(NULL);
}
void* client_recv_message(void* arg)
{
pthread_detach(pthread_self());//自动销毁线程函数,pthread_self为当前线程id
int client_socket = *(int*)arg;
char msg[512] = "";
while (1) {
ssize_t str_len = read(client_socket, msg,sizeof(msg));
if (str_len <= 0) {
break;
}
fputs(msg, stdout);//输出
memset(msg, 0, strlen(msg));
}
close(client_socket);
sem_post(&semid);//信号量+1
pthread_exit(NULL);
}
void Client()
{
int client_socket;
struct sockaddr_in server_addr;
client_socket = socket(PF_INET, SOCK_STREAM, 0);
//初始化
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_family = AF_INET;//
server_addr.sin_port = htons(9000);//端口
printf("[ input name ]: ");
scanf("%s", name);
fgetc(stdin);//读取最后一个\n,防止下次调用fgets 的时候读到换行符
printf("connect success\n");
if (connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr))==-1) {
printf("connect error\n");
close(client_socket);
return;
}
pthread_t send_id, recv_id;
sem_init(&semid, 0, -1);//初始化信号量为-1
//创建两个线程分别去处理发送群消息和接收群消息
pthread_create(&send_id, NULL, client_send_message, (void*)&client_socket);// 此处传的时局部变量的地址,必须保证子线程结束后,该线程才能结束
pthread_create(&recv_id, NULL, client_recv_message, (void*)&client_socket);//所以下面使用了信号量来等到两个子线程的结束
sem_wait(&semid);//等待上面两个线程结束,这里用信号量,初始信号量设为-1,上述2线程个执行一次sem_post,信号量变为1,此处就有信号了
close(client_socket);
}
void invoke(const char * arg)
{
if (strcmp(arg, "s") == 0) {//检查输入的命令行的第二个参数,如果为s,表示开启服务端,否则开启客户端
Server();
}
else {//其他任意字母
Client();
}
}
int main(int argc, char* argv[])
{
memset(client_socks, -1, sizeof(client_socks));//初始化全为无效套接字
invoke(argv[1]);//传入命令行的输入参数
return 0;
}
标签:addr,socket,--,server,client,线程,pthread,多线程,客户端
From: https://blog.csdn.net/qq_68245364/article/details/136912165