1.示例描述
因为最近在学多线程,并且用多线程做了一个“多人聊天小程序”,个人认为是吃透这个小程序,注释十分详细,所以需要先行记录下来,多线程的基础知识可以在示例之前给出。这个示例是基于TCP的socket多线程程序,实际上这个例子是综合使用socket、多线程和互斥锁,也就是“每来一个客户端连接,就开一个多线程去提供服务”,而在访问临界区资源的时候,就用互斥锁上锁,这样就把socket、多线程和互斥锁结合使用了。具体描述如下:
1)多个终端运行客户端程序,连接上服务器端;连接之后,可以发送消息
2)服务器端收到客户端发送的消息之后,将该消息广播给客户端
3)每个客户端都可以收到上述该条信息。
综述的效果类似于一个群聊。
下面分别从服务器端和客户端来实现socket程序。
2.服务器端
服务器端的主线程(main函数)代码结构和第一篇文章的socket编程框架是一致的:
1)绑定和监听;
2)开一个循环,不断提供接受连接请求;
3)每来一个连接请求,就开启一个线程去实现数据交互;
4)用线程分离数据交互的功能(t_main函数),对于每个连接,先读取客户端发送过来的数据,然后广播给所有客户端(send_msg_all函数);
5)断开连接则需要将对应的文件描述符从记录数组中移除,之后的广播则不会发送给该客户端了。
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#define MAX_CONNECTION 100
//线程入口函数
void* t_main(void*);
//发消息给所有客户端
void send_msg_all(int, char*, int);
//因为有很多个用来连接产生的sockfd,服务器端要做的事是:将所有连接通道里的信息都取出来,然后打包在一起返回给每个客户端
//所以,使用数据来存储所有的连接
int socks[MAX_CONNECTION];
int sock_count = 0;//记录连接的个数
//使用互斥量加锁
pthread_mutex_t mutex;
int main()
{
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
std::cout << "看门狗sokcet的sockfd: " << sockfd << std::endl;
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (bind(sockfd, (sockaddr*)&addr, sizeof(addr)) == -1)
{
std::cout << "socket bind error..." << std::endl;
return 0;
}
if (listen(sockfd, 5) == -1)
{
std::cout << "socket listen error..." << std::endl;
return 0;
}
while (true)
{
sockaddr_in svr_addr;
socklen_t len = sizeof(svr_addr);
//accept函数返回的文件描述符是 客户端的sockfd
//为什么accept函数的第三个参数是指针呢,因为可以修改传进来的地址长度
int clt_sockfd = accept(sockfd, (sockaddr*)&svr_addr, &len);
if (clt_sockfd == -1)
{
std::cout << "socket accept error..." << std::endl;
return 0;
}
//将成功连接的sock,用全局变量存储
//又因为是对全局变量的访问,安全起见(因为不是线程内部?),加锁
pthread_mutex_lock(&mutex);
std::cout << "有一个客户端连接上来了, sockfd = " << clt_sockfd << std::endl;
socks[sock_count++] = clt_sockfd;
std::cout << "现在的连接数: " << sock_count << ", sockfd分别是: ";
for (int i = 0; i < sock_count; i++)
std::cout << socks[i] << ", ";
std::cout << std::endl;
pthread_mutex_unlock(&mutex);
//只有有客户端连接上来了,就开启一个线程进行连接
pthread_t tid;
pthread_create(&tid, nullptr, t_main, &clt_sockfd);
//pthread_join(tid, nullptr);
//用pthread_detach来引导线程的销毁
pthread_detach(tid);
//std::cout << "client ip: " << inet_ntoa(svr_addr.sin_addr) << std::endl;
}
close(sockfd);
}
//在线程函数中,进行数据交互
void* t_main(void* arg)
{
int sockfd = *((int*)arg);
//从当前线程服务的客户端连接通道里,读取数据出来
char msg[BUFSIZ];
/*
而第一种写法由于操作符的优先级问题,导致size变量不能正确地反映读取的字节数,这会引起逻辑错误。
在编写涉及赋值和比较操作的循环条件时,正确使用括号至关重要,以确保操作的顺序符合预期
注意: !=操作符 优先于 =操作符执行
*/
int size = 0;//size要在循环之前定义啊....
//记住现在使用的是多线程,此时这线程内部的代码应该对于所有客户端的连接都会执行
//所以,以下循环是在 从某个连接通道里不断地读取数据(即客户端在不停地往通道里写数据)
//所以,当客户端断开连接了,即跳出循环
while ((size = read(sockfd, msg, sizeof(msg))) != 0)
{
//std::cout << "sizeof(buff) = " << sizeof(msg) << ", size = " << size << std::endl;
//将从这个连接通道读取出来的数据发送给 所有客户端
send_msg_all(sockfd, msg, size);
}
//所以,断开连接之后,顺序执行到了以下代码
//接下来的代码要做的事是: 移除断开连接的sock
pthread_mutex_lock(&mutex);
for (int i = 0; i < sock_count; i++)
{
if (sockfd == socks[i])
{
std::cout << "sockfd = " << sockfd << ", 已经断开连接..." <<std::endl;
//移除sockfd指向的连接通道
while(i < sock_count - 1)
{
socks[i] = socks[i + 1];
i++;
}
break;
}
}
sock_count--;
std::cout << "移除断开连接的socket之后, socks: ";
for(int i = 0; i < sock_count; i++)
std::cout << socks[i] << ", ";
std::cout << std::endl;
pthread_mutex_unlock(&mutex);
//关闭sock放在线程函数里
close(sockfd);
return nullptr;
}
//类似于广播,将当前线程服务的客户端传输过来的数据 广播发送给每个已连接的客户端
void send_msg_all(int sockfd, char* buff, int size)
{
std::cout << "从sockfd = " << sockfd << " 读取到数据: " << buff << std::endl;
pthread_mutex_lock(&mutex);
for (int i = 0; i < sock_count; i++)
{
//std::cout << "there are some connection..." << std::endl;
write(socks[i], buff, size);
}
pthread_mutex_unlock(&mutex);
}
3.客户端
客户端的代码结构和第一篇文章的socket编程框架是一致的:
1)注册服务器端地址信息,请求连接;
2)使用线程分离数据交互:开启两个线程分离读写(send_msg和recv_msg函数);
3)send_msg线程是输入客户端的消息,发送给服务器端;
4)recv_msg线程是从服务器端接收广播消息,打印出来。
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
//使用多线程实现读写分离
void* send_msg(void*);
void* recv_msg(void*);
//用来接收客户端程序的命令行参数
std::string name;
//似乎不用加锁
pthread_mutex_t mutex;
int main(int argc, char* argv[])
{
name = argv[1];
pthread_mutex_init(&mutex, nullptr);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
std::cout << name << "对应的sockfd是" << sockfd << std::endl;
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (connect(sockfd, (sockaddr*)&addr, sizeof(addr)) == -1)
{
std::cout << "connect error..." << std::endl;
return 0;
}
std::cout << "connect successfully...." << std::endl;
pthread_t thread_send, thread_recv;
pthread_create(&thread_send, nullptr, send_msg, &sockfd);
pthread_create(&thread_recv, nullptr, recv_msg, &sockfd);
//为什么不能用 pthread_detach ?
//因为使用 pthread_detach 将主线程和子线程分离了,那么主线程继续顺序执行
//执行到close(sockfd); 直接断开连接了。所以主线程必须等待数据交互完成
// pthread_detach(thread_send);
// pthread_detach(thread_recv);
pthread_join(thread_send, nullptr);
pthread_join(thread_recv, nullptr);
std::cout << "close connection..." << std::endl;
close(sockfd);
}
//将客户端输入的数据 发送给服务器端
void* send_msg(void* arg)
{
std::cout << "send thread main..." << std::endl;
int sockfd = *((int*)arg);
while (true)
{
std::string msg;
getline(std::cin, msg);//丢弃了换行
std::string name_msg = "[" + name + "]: " + msg;//替换了C的格式化输入
//std::cout << "name_msg: " << name_msg.c_str() << ", size = " << name_msg.size() << std::endl;
//pthread_mutex_lock(&mutex);
write(sockfd, name_msg.c_str(), name_msg.size());
//pthread_mutex_unlock(&mutex);
}
return nullptr;
}
//接收服务器端广播的消息
void* recv_msg(void* arg)
{
std::cout << "recv thread main..." << std::endl;
int sockfd = *((int*)arg);
//std::string msg;
char buff[BUFSIZ];
//std::cout << "in recv thread main..." << std::endl;
//int size = 0;
while (true)
{
//int all_size = 0;
// while ((size = read(sockfd, buff, BUFSIZ - 1)) != 0)
// {
// all_size += size;
// }
//因为服务器端每次只广播一条数据,因此可以不用循环来接收
int size = read(sockfd, buff, BUFSIZ - 1);
buff[size] = 0;
std::cout << buff << std::endl;
}
return nullptr;
}
4.效果图
以下图片是我开了一个服务器端,六个客户端来模拟多人聊天的截图。截图里面的输入输出信息十分详细地描述了客户端连接到服务器端,然后发送聊天消息给服务器端,服务器端作为中转 广播给所有连接的客户端,这样一个多人聊天的效果。
谈谈个人收获:
1)首先是掌握了socket编程的框架,能够很快、很清晰地写出socket相关的代码;
2)然后在以上代码中,嵌入多线程相关的代码。这就考虑到需要用多线程分离的功能,分别是服务器端用一个线程分离了读写功能,客户端用两个线程分别分离了读写;
3)在线程入口函数中,实现对应的功能。有点类似自顶向下的面向过程编程;
4)最后,在编写代码的过程中,我发现其实比较难的是read/write等读写函数的使用,这其实是socket通信编程的核心。而其中,我认为更难的使用的是read之类的读取数据函数。因为目前的数据很简单,没有用到数据结构,所以也不需要定义协议。如果需要定义协议,那么read的读取就显得很重要了。