视频解析:【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的
#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<string>
#include<cstring>
const static uint16_t defalu_port = 888;
const static std::string defaul_ip = "127.0.0.1";
const static int default_buf_size = 1024;
void Usage(std::string proc)
{
std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string _ip = argv[1];
uint16_t _port = std::stoi(argv[2]);
//第一步确定协议版本
WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
HIBYTE(wsadata.wVersion) != 2)
{
printf("确定网络协议版本失败!\n");
system("pause");
return 1;
}
else printf("确定网络协议版本成功!\n");
int sockedfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockedfd < 0)
{
std::cout << "create socket failed" << std::endl;
return 1;
}
//1.创建socket
int _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //报式套接字,第三个参数默认UDP协议
if (_sockfd == -1)
{
printf("create socket failed,socket error:%d\n", errno);
exit(SOCKET_ERROR);
}
printf("create socket success,sockfd:%d\n", _sockfd);
//2.确定协议ip
struct sockaddr_in server;
server = { 0 };
server.sin_family = AF_INET;
server.sin_port = htons(_port);
server.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());
while (true)
{
//向服务器发送数据
std::string send_buf;
std::cout << "请输入要发送的数据:" << std::endl;
std::getline(std::cin, send_buf);
size_t n = sendto(_sockfd, send_buf.c_str(), send_buf.size(), 0, (struct sockaddr*)&server, sizeof(server));
//发送完毕,接收服务器返回的数据
if (n > 0)
{
struct sockaddr_in remote;
socklen_t addr_len = sizeof(remote);
char recv_buf[default_buf_size];
int len = recvfrom(_sockfd, recv_buf, default_buf_size, 0, (struct sockaddr*)&remote, &addr_len);
if (len < 0)
{
printf("recvfrom error:%d\n", errno);
break;
}
else
{
recv_buf[len] = 0;
printf("recvfrom success,data:%s\n", recv_buf);
}
}
closesocket(_sockfd);
}
return 0;
}
【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的
网络视频:socket到底是什么?_哔哩哔哩_bilibili
大丙解析:13-客户端连接服务器函数 connect_哔哩哔哩_bilibili
目录
视频解析:【C/C++网络编程】多客户端聊天室!多线程+网络编程实现多人聊天功能,可以实现多客户端的简单通讯 ~_哔哩哔哩_bilibili
网络视频:socket到底是什么?_哔哩哔哩_bilibili
大丙解析:13-客户端连接服务器函数 connect_哔哩哔哩_bilibili
前置知识
客户端
服务器端进行网络通信的流程
1.初始化套接字环境
2.创建Socket
3.确定服务器协议地址簇
4.绑定
5.监听
6.接收客户端链接
7.通信
8.关闭socket
1.初始化套接字环境
先声明一个WSAData类型的变量
WSAData wsadata;
然后输入:"WSAStartup",选中它,按F1会转到帮助文档. 文档里说明了要使用WSAStartup的要求:
通过MAKEWORD这个宏指定主版本为2,副版本为2的协议:
然后加上判断
WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
HIBYTE(wsadata.wVersion) != 2)
{
printf("确定网络协议版本失败!\n");
system("pause");
return -1;
}
else printf("确定网络协议版本成功!\n");
2.创建Socket
接口:socket():
第一个参数指定要使用的ip协议是Ipv4还是ipv6.
这由两个宏定义:
其中AF_UNIX和AF_LOCAL不同于基于网络的套接字如 AF_INET
(用于IPv4)或 AF_INET6
(用于IPv6),它并不通过网络进行数据交换,而是利用了操作系统的内核机制,在进程之间直接传递数据。。
- 参数
- 第二个参数int type指定了要使用的协议是流式协议还是报式协议。
- 流式协议的宏定义为SOCK_STREAM,报式协议的宏定位为SOCK_DGRAM。
- 第三个参数指定使用什么协议,流式协议默认使用Tcp,报式协议默认使用udp。
- 返回值
- 如果成功就会创建一个文件描述符返回,如果失败就会返回-1
- 如果成功就会创建一个文件描述符返回,如果失败就会返回-1
SOCKET socketserver = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (SOCKET_ERROR == socketserver)
{
printf("创建socket失败,%d\n",GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("创建socket成功!\n");
3.确定服务器协议地址簇
SOCKADDR_IN就是一个结构体,里面有IP地址,端口号这些成员变量.一个IP地址确定一台主机,端口号确定APP;
SOCKADDR_IN addr = { 0 };
addr.sin_family = AF_INET; //照抄上面的
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //ip地址
addr.sin_port = htons(9527); //端口号
4.绑定
绑定接口函数:int bind():
- 参数
- 第一个参数是套接字描述符。
- 第二个参数是一个结构体类型的参数,用来存放要绑定的ip地址和端口号。结构体定义如下:
- 第三个参数是前面结构体的大小。我们用sizeof()计算出大小后填写到第三个参数即可。
- 返回值
- 绑定成功返回0,失败返回-1
- 绑定成功返回0,失败返回-1
int r = bind(socketserver,(sockaddr*)&addr,sizeof addr);
if (-1== r)
{
printf("绑定失败,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("绑定成功!\n");
5.监听
- 参数
- 第一个参数就是文件描述符,通过调用socket()得到。
- 第二个参数是指定要绑定的客户端的最大连接个数。最大个数为128个。
注意,这里的的最大连接个数128不是指总共只能连128个客户端机子,而是说一次只能最多连128个客户端机子。n个客户端机子可以分多次进行连接。
- 返回值
- 监听成功返回0,失败返回-1
- 监听成功返回0,失败返回-1
r = listen(socketserver,10);
if (-1 == r)
{
printf("监听失败,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("监听成功!\n");
6.接受客户端连接
- 参数
- 第一个参数是文件描述符,通过调用socket()得到。
- 第二个参数仍然是一个结构体类型的指针,用来存储要绑定的客户端的ip地址和端口号。
- 第三个参数指定第二个参数的大小,用sizeof()计算一下大小即可。
- 返回值
- 函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
- 函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
SOCKET clientSocket = accept(socketserver, (sockaddr*)NULL, NULL); //后两个参数是客户端的ip,端口号
if (-1 == clientSocket)
{
printf("服务器崩溃,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("有客户端链接服务器\n");
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
7.通信
- 通信函数
- 接收 recv/read
- 发送
- 接收 recv/read
char buff[56]; //接收客户端发来的信息
while (1)
{
r = recv(clientSocket, buff, 55, NULL); //最多接收55条,因为还要留一个给结束符
if (r > 0) //如果接收到消息了就添加一个结束符
{
buff[r] = 0;
printf(">>%s\n", buff);
}
}
8.关闭socket
closesocket(socketserver);
9.清理协议
WSACleanup();
客户端进行网络通信的流程
1.确定协议版本
2.创建Socket
3.确定服务器协议地址簇
4.连接服务器
5.通信
6.关闭socket
7.清理协议
1,2,3步骤和服务器端一样,第四个步骤如下:
4.连接服务器
- 参数
- 同accept
我们注意到第二个参数加了const限定,说明这是一个输入性参数。
- 返回值
我们注意看返回值说的是如果绑定或者连接成功就会返回0,失败则返回-1。客户端也要绑定吗?
答案是是的,客户端也需要绑定。那么这个要绑定的ip和端口号是什么呢?
实际上绑定的是一个随机的没有被占用的端口号,这个是不需要我们去手动操作的。
先前我们服务器端绑定的时候是我们手动绑定一个固定ip和端口号,为什么到客户端这里就只需要随机绑定一个端口号了呢?
因为服务器端是优于客户端先创建的,那么客户端如果想向服务器端发送请求就需要知道服务器端的ip地址和端口号,因此服务器端需要一个固定的ip地址和端口号。
服务器端绑定固定ip和客户端成功之后,客户端通过connect()自动与这个固定的ip和端口号连接。
之后服务器端通过accept()函数接受客户端连接,同时接受到了客户端的ip和端口号,然后就可以与客户端进行通信了。
如果我们想让服务器端与客户端的固定的端口号进行通信,那我们在客户端也需要调用bind()函数绑定指定端口号。
int r = connect(socketserver, (sockaddr*)&addr, sizeof addr);
if (-1 == r)
{
printf("连接服务器失败!\n");
WSACleanup(); //如果失败就把协议清理掉
system("pause");
}
else printf("连接服务器成功!\n");
5.通信
- 通信函数
- 接收 recv/read
- 发送
- 接收 recv/read
//循环接收用户输入,发送给服务器
char buff[56];
while (1)
{
scanf("%s", buff);
send(socketserver, buff, strlen(buff), NULL);
}
6.关闭socket
closesocket(socketserver);
7.清理协议
WSACleanup();
此时,只能做到客户端与服务器端一对一通信:
我们现在想实现多个客户端与服务器端进行通信,就要用到io多路复用,多线程等技术.我们这里用多线程.
我们在接收客户端连接这一部分进行修改:
我们将上文定义的SOCKET clientSocket变为SOCKET clientSocket[].这样就可以接受多条客户端的链接.我们设置一个for循环,在循环中通过下标自增来接受多个客户端链接的请求,代码如下:
for (int i = 0; i < NUM; i++) //可以接受NUM个客户端的链接
{
clientSocket[i] = accept(socketserver, (sockaddr*)NULL, NULL); //后两个参数是客户端的ip,端口号
if (-1 == clientSocket[i])
{
printf("服务器崩溃,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("有客户端链接服务器\n");
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
}
这样我们就可以多个客户端与服务器端进行通信了:
代码整理
客户端
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS //为了消除警告
#include<stdio.h>
#include <winsock2.h>
#include<windows.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
//第一步确定协议版本
WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
HIBYTE(wsadata.wVersion) != 2)
{
printf("确定网络协议版本失败!\n");
system("pause");
return -1;
}
else printf("确定网络协议版本成功!\n");
//第二步创建Socket
SOCKET socketserver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SOCKET_ERROR == socketserver)
{
printf("创建socket失败,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("创建socket成功!\n");
//3.确定服务器协议地址簇
SOCKADDR_IN addr = { 0 }; //SOCKADDR_IN就是一个结构体
addr.sin_family = AF_INET; //照抄上面的
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //ip地址
addr.sin_port = htons(9527); //端口号
//4.连接服务器
int r = connect(socketserver, (sockaddr*)&addr, sizeof addr);
if (-1 == r)
{
printf("连接服务器失败!\n");
WSACleanup(); //如果失败就把协议清理掉
system("pause");
}
else printf("连接服务器成功!\n");
//5.通信
//循环接收用户输入,发送给服务器
char buff[56];
while (1)
{
scanf("%s", buff);
send(socketserver, buff, strlen(buff), NULL);
}
//6.关闭socket
closesocket(socketserver);
//7.清理协议
WSACleanup();
return 0;
}
服务器端
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS //为了消除警告
#include<stdio.h>
#include <winsock2.h>
#include<windows.h>
#pragma comment(lib, "ws2_32.lib")
#define NUM 1024
SOCKET clientSocket[1024];
void tongxun(int idx);
int main()
{
//第一步确定协议版本
WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
HIBYTE(wsadata.wVersion) != 2)
{
printf("确定网络协议版本失败!\n");
system("pause");
return -1;
}
else printf("确定网络协议版本成功!\n");
//第二步创建Socket
SOCKET socketserver = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (SOCKET_ERROR == socketserver)
{
printf("创建socket失败,%d\n",GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("创建socket成功!\n");
//3.确定服务器协议地址簇
SOCKADDR_IN addr = { 0 }; //SOCKADDR_IN就是一个结构体
addr.sin_family = AF_INET; //照抄上面的
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //ip地址
addr.sin_port = htons(9527); //端口号
//4.绑定
int r = bind(socketserver,(sockaddr*)&addr,sizeof addr);
if (-1== r)
{
printf("绑定失败,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("绑定成功!\n");
//5.监听
r = listen(socketserver,10);
if (-1 == r)
{
printf("监听失败,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("监听成功!\n");
//6.接收客户端链接
for (int i = 0; i < NUM; i++) //可以接受NUM个客户端的链接
{
clientSocket[i] = accept(socketserver, (sockaddr*)NULL, NULL); //后两个参数是客户端的ip,端口号
if (-1 == clientSocket[i])
{
printf("服务器崩溃,%d\n", GetLastError());
WSACleanup(); //如果失败就把协议清理掉
system("pause");
return -1;
}
else printf("有客户端链接服务器\n");
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)tongxun, (LPVOID)i, NULL, NULL);
}
//8.关闭socket
closesocket(socketserver);
//9.清理协议
WSACleanup();
return 0;
}
void tongxun(int idx)
{
int r;
//7.通信
char buff[56]; //接收客户端发来的信息
while (1)
{
r = recv(clientSocket[idx], buff, 55, NULL); //最多接收55条,因为还要留一个给结束符
if (r > 0) //如果接收到消息了就添加一个结束符
{
buff[r] = 0;
printf(">>%s\n", buff);
}
}
}
udp通信
服务器端
首先创建一个udpserver.hpp文件。该文件就是udp通信的服务器端
udpserver.hpp
#pragma once
#include<iostream>
#include"nocopy.hpp"
// static const uint16_t=888;
class UdpServer : public nocopy
{
public:
UdpServer(std::string& ip, uint16_t prot) :_ip(ip), _port(prot) {}
~UdpServer() ;
void Init() ;
void Start() ;
private:
std::string _ip;
uint16_t _port;
};
我们在该文件中声明一个UdpServer类,类内定义两个成员变量:ip和prot。构造函数和析构,以及Init()和Start()函数。
建立Main.cc文件
Main.cc
首先我们声明一个智能指针对象,用它来管理内存释放。
#include<iostream>
#include<memory>
#include"udpserver.hpp"
#include"Common.hpp"
#include<string>
int main(int argc, char* argv[])
{
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
usvr->Init();
usvr->Start();
return 0;
}
我们想在程序运行时就输入ip和prot参数。
如果用户不会使用,没有传入ip和prot。我们就引入一个报错机制,提示用户应该传入ip和prot。
Main.cc
void Usage(std::string proc)
{
std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return Usage_Err;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
return 0;
}
Common.hpp
#pragma once
enum {
Usage_Err = 1
};
为了保证服务器端只存在一份,不被拷贝。我们新建一个nocopy类,在该类内部把拷贝构造和赋值重载禁了。然后让UdpServer类继承nocopy类,这样UdpServer类同样也不能被拷贝和赋值。
#pragma once
#include<iostream>
class nocopy
{
public:
nocopy() {}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
~nocopy() {}
};
此时运行程序,如果没有输入ip和Port就会打印错误码,然后提示传入ip和port:
否则,就会正常执行:
通信的几个步骤
udpserver.hpp
服务器端启动:
客户端
客户端和服务端类似,但是客户端不需要显示绑定,因为客户端的ip是随机绑定的。
#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#pragma comment(lib, "Ws2_32.lib")
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<string>
#include<cstring>
const static uint16_t defalu_port = 888;
const static std::string defaul_ip = "127.0.0.1";
const static int default_buf_size = 1024;
void Usage(std::string proc)
{
std::cout << "Usage\t" << "local-ip, local_port\n" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string _ip = argv[1];
uint16_t _port = std::stoi(argv[2]);
//第一步确定协议版本
WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
if (LOBYTE(wsadata.wVersion) != 2 ||
HIBYTE(wsadata.wVersion) != 2)
{
printf("确定网络协议版本失败!\n");
system("pause");
return 1;
}
else printf("确定网络协议版本成功!\n");
int sockedfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockedfd < 0)
{
std::cout << "create socket failed" << std::endl;
return 1;
}
//1.创建socket
int _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //报式套接字,第三个参数默认UDP协议
if (_sockfd == -1)
{
printf("create socket failed,socket error:%d\n", errno);
exit(SOCKET_ERROR);
}
printf("create socket success,sockfd:%d\n", _sockfd);
//2.确定协议ip
struct sockaddr_in server;
server = { 0 };
server.sin_family = AF_INET;
server.sin_port = htons(_port);
server.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());
while (true)
{
//向服务器发送数据
std::string send_buf;
std::cout << "请输入要发送的数据:" << std::endl;
std::getline(std::cin, send_buf);
size_t n = sendto(_sockfd, send_buf.c_str(), send_buf.size(), 0, (struct sockaddr*)&server, sizeof(server));
//发送完毕,接收服务器返回的数据
if (n > 0)
{
struct sockaddr_in remote;
socklen_t addr_len = sizeof(remote);
char recv_buf[default_buf_size];
int len = recvfrom(_sockfd, recv_buf, default_buf_size, 0, (struct sockaddr*)&remote, &addr_len);
if (len < 0)
{
printf("recvfrom error:%d\n", errno);
break;
}
else
{
recv_buf[len] = 0;
printf("recvfrom success,data:%s\n", recv_buf);
}
}
closesocket(_sockfd);
}
return 0;
}
运行之后输入127.0.0.1 8888
效果:
现在服务器端不止想知道客户端发来了什么,我还想知道客户端ip和端口号,并将其打印出来:
现在我想把获取解析客户端ip和port这一部分单独封装到一个类里放另一个文件,提供接口出来:
PortIp.hpp
#pragma once
#include"nocopy.hpp"
#include"winsock2.h"
#include"ws2tcpip.h"
#include<iostream>
#include<cstring>
#include <string>
class PortIp {
public:
PortIp(struct sockaddr_in& client_addr )
{
_port = ntohs(client_addr.sin_port);
_ip = inet_ntoa(client_addr.sin_addr);
}
std::string GetIp() { return _ip; }
uint16_t GetPort() { return _port; }
std::string Print()
{
std::string info = std::string(_ip) + ':' + std::to_string(_port);
return info;
}
~PortIp() {}
private:
uint16_t _port;
char* _ip;
};
效果: