一个服务器作为中转站,多个客户端之间可以相互通信。至少需要启动两个客户端。
三个客户端互相通信
chatServer.cpp
函数:socket()、bind()、listen()、accept()、read()、write()
#include
#include
#include
#include //epoll的头文件
#include //socket的头文件
#include //close()的头文件
#include //包含结构体 sockaddr_in
#include //保存客户端信息
#include //提供inet_ntoa函数
using namespace std;
const int MAX_CONNECT = 5; //全局静态变量,允许的最大连接数
struct Client{
int sockfd; //socket file descriptor 套接字文件描述符
string username;
};
int main(){
//创建一个epoll实例
int epfd = epoll_create1(0); //或老版本 epoll_create(1);
if(epfd < 0){
perror("epoll create error");
return -1;
}
//创建监听的socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){ //若socket创建失败,则返回-1
perror("socket error");
return -1;
}
//绑定本地ip和端口
struct sockaddr_in addr; //结构体声明,头文件是
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9999);
int ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret < 0){
printf("bind error\n");
cout << "该端口号已被占用,请检查服务器是否已经启动。" << endl;
return -1;
}
cout << "服务器中转站已启动,请加入客户端。" << endl;
//监听客户端
ret = listen(sockfd,1024);
if(ret < 0){
printf("listen error\n");
return -1;
}
//将监听的socket加入epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev); //防御性编程,方便出bug时快速定位问题
if(ret < 0){
printf("epoll_ctl error\n");
return -1;
}
//保存客户端信息
map<int,Client> clients;
int clientCount = 0; //添加一个客户端计数器
//循环监听
while(true){
struct epoll_event evs[MAX_CONNECT];
int n = epoll_wait(epfd,evs,MAX_CONNECT,-1);
if(n < 0){
printf("epoll_wait error\n");
break;
}
for(int i = 0; i < n; i ++){
int fd = evs[i].data.fd;
//如果是监听的fd收到消息,则表示有客户端进行连接了
if(fd == sockfd){
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_sockfd = accept(sockfd, (struct sockaddr*) & client_addr, &client_addr_len);
if(client_sockfd < 0){
printf("accept error,连接出错\n");
continue;
}
//将客户端的socket加入epoll
struct epoll_event ev_client;
ev_client.events = EPOLLIN; //检测客户端有没有消息过来
ev_client.data.fd = client_sockfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD,client_sockfd,&ev_client);
if(ret < 0){
printf("epoll_ctl error\n");
break;
} //iner_ntoa() 将客户端的IP地址从网络字节顺序转换为点分十进制字符串
clientCount++; //有新的客户端加入时,增加计数器
printf("客户端%d已连接: IP地址为 %s\n", clientCount, inet_ntoa(client_addr.sin_addr));
//保存该客户端信息
Client client;
client.sockfd = client_sockfd;
client.username = "";
clients[client_sockfd] = client;
}else{
char buffer[1024];
int n = read(fd, buffer, 1024);
if(n < 0){
break; //处理错误
}else if(n == 0){
//客户端断开连接
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL, fd ,0);
clients.erase(fd);
}else{ // n > 0
string msg(buffer,n);
//如果该客户端username为空,说明该消息是这个客户端的用户名
if(clients[fd].username == ""){
clients[fd].username = msg;
}else{
string name = clients[fd].username;
//把消息发给其他所有客户端
for(auto &c:clients){
if(c.first != fd){
string full_message = '[' + name + ']' + ':' + msg;
write(c.first, full_message.c_str(), full_message.length());
//write(c.first,('[' + name + ']' + ":" + msg).c_str(),msg.size() + name.size() + 4);
}
}
}
}
}
}
}
//关闭epoll实例
close(epfd);
close(sockfd);
return 0;
}
client.cpp (注意g++编译时要加 -pthread)
函数:socket()、connect()、send()、recv()
#include
#include
#include //memset()的头文件
#include //socket(),connect()等函数的头文件
#include //sockaddr_in的头文件
#include //inet_pton()函数的头文件
#include //close()函数的头文件
#include //pthread创建线程和管理线程的头文件
using namespace std;
#define BUF_SIZE 1024
char szMsg[BUF_SIZE];
//发送消息
void* SendMsg(void *arg){
int sock = *((int*)arg);
while(1){
//scanf("%s",szMsg);
fgets(szMsg,BUF_SIZE,stdin); //使用fgets代替scanf
if(szMsg[strlen(szMsg) - 1] == '\n'){
szMsg[strlen(szMsg)- 1] = '\0'; //去除换行符
}
if(!strcmp(szMsg,"QUIT\n") || !strcmp(szMsg,"quit\n")){
close(sock);
exit(0);
}
send(sock, szMsg, strlen(szMsg), 0);
}
return nullptr;
}
//接收消息
void* RecvMsg(void * arg){
int sock = *((int*)arg);
char msg[BUF_SIZE];
while(1){
int len = recv(sock, msg, sizeof(msg)-1, 0);
if(len == -1){
cout << "系统挂了" << endl;
return (void*)-1;
}
msg[len] = '\0';
printf("%s\n",msg);
}
return nullptr;
}
int main()
{
//创建socket
int hSock;
hSock = socket(AF_INET, SOCK_STREAM, 0);
if(hSock < 0){
perror("socket creation failed");
return -1;
}
//绑定端口
sockaddr_in servAdr;
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_port = htons(9999);
if(inet_pton(AF_INET, "172.16.51.88", &servAdr.sin_addr) <= 0){
perror("Invalid address");
return -1;
}
//连接到服务器
if(connect(hSock, (struct sockaddr*)&servAdr, sizeof(servAdr)) < 0){
perror("连接服务器失败");
cout << "请检查是否已启动服务器。" << endl;
return -1;
}else{
printf("已连接到服务器,IP地址:%s,端口:%d\n", inet_ntoa(servAdr.sin_addr), ntohs(servAdr.sin_port));
printf("欢迎来到私人聊天室,请输入你的聊天用户名:");
}
//创建线程
pthread_t sendThread,recvThread;
if(pthread_create(&sendThread, NULL, SendMsg, (void*)&hSock)){
perror("创建发送消息线程失败");
return -1;
}
if(pthread_create(&recvThread, NULL, RecvMsg, (void*)&hSock)){
perror("创建接收消息线程失败");
return -1;
}
//等待线程结束
pthread_join(sendThread, NULL);
pthread_join(recvThread, NULL);
//关闭socket
close(hSock);
return 0;
}
#include
#include
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()成功返回0,失败返回-1
以下是一个简单的 TCP 客户端示例,展示了如何使用 connect() 连接到服务器:
#include
#include
#include
#include
#include
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345); // 服务器监听的端口
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); // 服务器的IP地址
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
return -1;
}
printf("Connected to the server\n");
// 之后可以使用sockfd进行数据传输
close(sockfd); // 关闭套接字
return 0;
}
在这个示例中,客户端程序创建了一个套接字,设置服务器的 IP 地址和端口,然后尝试与服务器建立连接。如果 connect() 调用成功,客户端就与服务器建立了连接,并可以通过该套接字进行数据通信。
1.参数
int socket(int address_family, int type, int protocol);
(1)address family:
①AF_INET:IPv4 网络协议。用于TCP/IP和UDP/IP网络通信。
②AF_INET6:IPv6 网络协议。用于TCP/IP和UDP/IP网络通信,但支持IPv6地址。
③AF_UNIX(或AF_LOCAL):本地通道通信。用于在同一台机器上的进程间通信。
2)type:
①SOCK_STREAM:TCP协议,提供面向连接的稳定数据传输,保证数据能够按顺序、完整地到达。
②SOCK_DGRAM:UDP协议,提供无连接的数据传输服务。发送的是独立的消息,不保证顺序或数据完整性。
③SOCK_RAW:提供原始网络协议访问。在网络模型中,这种类型的套接字允许直接访问IP层,通常用于网络协议的开发和测试。
(3)protocol:默认协议填0
2.返回值:
①成功时,socket() 返回一个非负整数,即新创建的套接字文件描述符。
②出错时,返回 -1,并设置全局变量 errno 以表示具体的错误类型。
3.创建一个使用IPv4地址和TCP协议的套接字:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
}
这里,AF_INET 指定使用IPv4地址,SOCK_STREAM 指定使用面向连接的数据传输方式(TCP),0 表示自动选择使用TCP协议。
发送数据:将数据放到发送缓冲区。由内核决定什么时候将数据发送出去。
接收数据:当数据送到Linux内核后,数据不是立即给到应用程序,而是放在接收缓冲区,等应用程序什么时候调用recv()函数,什么时候才由内核给到应用程序。
1.做的Linux端,只能在相同的IP上启动几个客户端自己玩。
后续可以做成Windows的exe,买个云服务器,然后发给朋友,进行通信。