2-3 建立简易TCP服务端、客户端
文章目录
0-前言
【C++百万并发网络通信】系列是跟着【张远东】老师的视频来复现的
希望能通过博客的方式不断坚持学习,也希望偶然间看到这篇博客的你也能一起加油!
笔记目录:【C++百万并发网络通信-笔记目录】
更新时间:
2020.12.30 完成服务端简易功能
2021.1.5 完成客户端简易功能,测试服务端与客户端成功
1-服务端简易功能
- 建立socket
- 绑定端口
bind
- 监听网络端口
listen
- 等待客户端连接
accept
- 向客户端发送数据
send
- 关闭socket
closesocket
注意这里省略了【接收客户端数据】recv
这一功能,即服务端只能【发】
2-客户端简易功能
- 建立socket
- 连接服务器
connect
- 接收服务器信息
recv
- 关闭socket
closesocket
注意这里省略了【向服务端发送数据】send
这一功能,即客户端只能【收】,这就好比我们有一部只能接电话的手机,不能打电话。
3-代码逻辑
#define WIN32_LEAN_AND_MEAN
#include<Windows.h>
#include<WinSock2.h>
#pragma comment(lib, "ws2_32.lib")//加入静态链接库
int main()
{
WORD ver = MAKEWORD(2, 2);//WORD版本号
WSADATA dat;//一种数据结构
//启动windows socket 2.x环境
WSAStartup(ver, &dat);
//-------------------
//--建立简易TCP客户端
// 1 建立socket
// 2 连接服务器 connect
// 3 接收服务器信息 recv
// 4 关闭socket closesocket
//--建立简易TCP服务端
// 1 建立socket
// 2 绑定端口 bind
// 3 监听端口 listen
// 4 等待客户端连接 accept
// 5 向客户端发送消息 send
// 6 关闭socket closesocket
//-------------------
//清除Windows socket环境
WSACleanup();//关闭windows socket网络环境
return 0;
}
4-服务端
首先开始在当前【解决方案】下,新建一个【项目】EasyTcpServer
,别忘了修改【输出目录】和【中间目录】,忘了可以看这里【VS2019新建项目、解决方案、多项目生成、防止文件污染】
重新生成新项目时,别忘了右键设为启动项目,如果报出下面的错误,就是#pragma那句忘了解注释,一定要解开如下:
4-1 建立socket
// 1 建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
socket()函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。
函数声明:int socket( int af, int type, int protocol);
af:一个地址描述。仅支持AF_INET格式,也就是说ARPA Internet地址格式。AF_INET代表IPv4格式的网络地址。
type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM(基于流)、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
参考:百度百科
若无错误发生,socket()返回引用新套接口的描述字。下图中能够看到,SOCKET
是一个uint类型的指针
4-2 绑定端口
// 2 绑定端口 bind
sockaddr_in _sin = {
};
_sin.sin_family = AF_INET;//必须与建立socket的af保持一致,表示地址类型
_sin.sin_port = htons(4567);//host to net unsigned short,将主机端口转换为网络端口
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//随意ip地址//inet_addr("127.0.0.1");//本机地址,防止外网访问
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
cout << "Error:绑定用于接收客户端连接的网络端口失败" << endl;
}
else
{
cout << "Success:绑定网络端口成功..." << endl;
}
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
函数声明:
bind(SOCKET s, const socketaddr *name, int namelen)
参数解释:
SOCKET s:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
const socketaddr *name:一个const struct sockaddr *指针,指向要绑定给SOCKET 的协议地址。
int namelen:对应的是地址的长度。
参考:socket–socket()、bind()、listen()、connect()、accept()、recv()、send()、select()、close()、shutdown()
4-2-1 sockaddr
那么为什么bind()
的第二个参数不直接使用sockaddr
类型而要使用一个sockaddr_in
类型再强制转换呢,这是因为sockaddr_in
类型中的变量类型都是常用的变量,方便赋值。
sockaddr在头文件
#include <sys/socket.h>
中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
4-2-2 sockaddr_in
sockaddr_in在头文件
#include<netinet/in.h>或#include <arpa/inet.h>
中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
4-3 监听端口
// 3 监听端口 listen
if (SOCKET_ERROR == listen(_sock, 5))
{
cout << "Error:监听网络端口失败" << endl;
}
else
{
cout << "Success:监听网络端口成功..." << endl;
}
SOCKET_ERROR : 如调用bind()、listen()、connect()、send()、setsockopt()、fcntl()等函数时出错则会返回该宏:
函数声明:int listen (int sockfd, int backlog);
该函数在bind()之后accept()调用之前调用。第一个参数为已经创建的监听socket, 第二个参数是socket 监听队列最大监听连接数。
4-4 等待客户端连接
// 4 等待客户端连接 accept
sockaddr_in clientAddr = {
};//远程客户端地址
int nAddrLen = sizeof(clientAddr);//结构长度
SOCKET _csock = INVALID_SOCKET;//无效的socket地址
_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);//核心
if (INVALID_SOCKET == _csock)
{
cout << "Error:接收到无效客户端socket..." << endl;
}
使用accept()
来接收客户端连接请求
函数声明:SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
addr用于存放客户端的地址,addrlen在调用函数时被设置为addr指向区域的长度
对于接收到的客户端socket,需要判断是否有效
4-5 发送数据
// 5 向客户端发送消息 send
char msgBuf[] = "Hello, I'm Server.";
send(_csock, msgBuf, strlen(msgBuf)+1, 0);
函数声明:int send(SOCKET s, const char *buf, int len, int flags);
SOCKET s:是本机要发送给谁的socket,本机是服务端,因此s就是要接收数据的客户端socket
const char *buf :应用程序要发送的数据的缓冲区(想要发送的数据)
int len:实际发送的字节数
int flags:一般置0
那么为什么int len
的位置是strlen(msgBuf)+1呢,因为想把字符数组最后一位结束符也发过去
那么现在经过【4-4】与【4-5】已经能够实现单个客户端的接入,那么怎么实现不断接入客户端呢:需要加入一个循环,来不断接受来自客户端的连接请求。整理【4-4】与【4-5】代码如下,实现不断接入客户端,并为接入的客户端发送一条消息的功能。
// 4 等待客户端连接 accept
sockaddr_in clientAddr = {
};//远程客户端地址
int nAddrLen = sizeof(clientAddr);//结构长度
SOCKET _csock = INVALID_SOCKET;//无效的socket地址
char msgBuf[] = "Hello, I'm Server.";
while (true)
{
_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == _csock)
{
cout << "Error:接收到无效客户端socket..." << endl;
}
cout << "新客户端加入:IP = " << inet_ntoa(clientAddr.sin_addr) << endl;
// 5 向客户端发送消息 send
send(_csock, msgBuf, strlen(msgBuf) + 1, 0);
}
这里面,使用了一个比较老的函数inet_ntoa()
,需要在程序开头定义一个宏
#define _WINSOCK_DEPRECATED_NO_WARNINGS
4-6 关闭socket
// 6 关闭socket closesocket
closesocket(_sock);
本函数关闭一个套接口。更确切地说,它释放套接口描述字s,以后对s的访问均以WSAENOTSOCK错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。
参考:百度百科
至此,完成TCP服务器的简易模型,生成项目成功
4-7 服务端全部程序
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")//加入静态链接库
int main()
{
WORD ver = MAKEWORD(2, 2);//WORD版本号
WSADATA dat;//一种数据结构
//启动windows socket 2.x环境
WSAStartup(ver, &dat);
//-------------------
//--建立简易TCP服务端
// 1 建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2 绑定端口 bind
sockaddr_in _sin = {
};//网络端口地址
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);//host to net unsigned short
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//随意ip地址//inet_addr("127.0.0.1");//本机地址,防止外网访问
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
cout << "Error:绑定用于接收客户端连接的网络端口失败" << endl;
}
else
{
cout << "Success:绑定网络端口成功..." << endl;
}
// 3 监听端口 listen
if (SOCKET_ERROR == listen(_sock, 5))
{
cout << "Error:监听网络端口失败" << endl;
}
else
{
cout << "Success:监听网络端口成功..." << endl;
}
// 4 等待客户端连接 accept
sockaddr_in clientAddr = {
};//远程客户端地址
int nAddrLen = sizeof(clientAddr);//结构长度
SOCKET _csock = INVALID_SOCKET;//无效的socket地址
char msgBuf[] = "Hello, I'm Server.";
while (true)
{
_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == _csock)
{
cout << "Error:接收到无效客户端socket..." << endl;
}
cout << "新客户端加入:IP = " << inet_ntoa(clientAddr.sin_addr) << endl;
// 5 向客户端发送消息 send
send(_csock, msgBuf, strlen(msgBuf) + 1, 0);
}
// 6 关闭socket closesocket
closesocket(_sock);
//-------------------
//清除Windows socket环境
WSACleanup();//关闭windows socket网络环境
return 0;
}
5-客户端
首先开始在当前【解决方案】下,新建一个【项目】EasyTcpClient
,别忘了修改【输出目录】和【中间目录】,忘了可以看这里【VS2019新建项目、解决方案、多项目生成、防止文件污染】
5-1 建立socket
具体步骤已经在【4-1】中描述
// 1 建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);//0:不规定协议类型
if (INVALID_SOCKET == _sock)
{
cout << "Error:建立Socket失败!" << endl;
}
else
{
cout << "建立Socket成功..." << endl;
}
5-2 连接服务器
这一小节与【4-2】特别像,都要传入一个socketaddr
的结构体,不同的是,服务端的bind()
所需要的各个参数都是服务器自身的,而客户端的有些参数是来自服务器的
// 2 连接服务器 connect
sockaddr_in _sin = {
}; //能够将结构体快速初始化
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);//客户端想要连接服务器的哪个端口
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//连接服务器的ip地址,127.0.0.1是本机地址
int res = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
if (SOCKET_ERROR == res)
{
cout << "Error:连接失败!" << endl;
}
else
{
cout << "连接成功..." << endl;
}
函数原型: int connect(SOCKET s, const struct sockaddr * name, int namelen);
s:标识一个未连接socket
name:指向要连接套接字的
sockaddr
结构体的指针namelen:
sockaddr
结构体的字节长度https://baike.baidu.com/item/connect%28%29/10081861?fr=aladdin
5-3 接收服务器信息
这一小节使用到数据缓冲,与【4-5】类似
// 3 接收服务器信息 recv
char recvBuf[256] = {
};//接收数据缓冲区
int nlen = recv(_sock, recvBuf, 256, 0);//recv()返回接收数据的长度
if (nlen > 0)
{
cout << "接收到数据:" << recvBuf << endl;
}
函数原型:int recv(SOCKET s, char *buf, int len, int flags);
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
5-4 关闭socket
// 4 关闭socket closesocket
closesocket(_sock);
5-5 客户端全部程序
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<Windows.h>
#include<WinSock2.h>
#include<iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")//加入静态链接库
int main()
{
WORD ver = MAKEWORD(2, 2);//WORD版本号
WSADATA dat;//一种数据结构
//启动windows socket 2.x环境
WSAStartup(ver, &dat);
//-------------------
//--建立简易TCP客户端
// 1 建立socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);//0:不规定协议类型
if (INVALID_SOCKET == _sock)
{
cout << "Error:建立Socket失败!" << endl;
}
else
{
cout << "建立Socket成功..." << endl;
}
// 2 连接服务器 connect
sockaddr_in _sin = {
}; //能够将结构体快速初始化
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);//客户端想要连接服务器的哪个端口
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//连接服务器的ip地址,127.0.0.1是本机地址
int res = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
if (SOCKET_ERROR == res)
{
cout << "Error:连接失败!" << endl;
}
else
{
cout << "连接成功..." << endl;
}
// 3 接收服务器信息 recv
char recvBuf[256] = {
};//接收数据缓冲区
int nlen = recv(_sock, recvBuf, 256, 0);//recv()返回接收数据的长度
if (nlen > 0)
{
cout << "接收到数据:" << recvBuf << endl;
}
// 4 关闭socket closesocket
closesocket(_sock);
//-------------------
//清除Windows socket环境
WSACleanup();//关闭windows socket网络环境
system("pause");
return 0;
}
6-测试最终程序
客户端与服务端全部生成之后,在我们设置的【输出目录】中找到对应的exe文件,如下图
注意:先打开【服务端】,再打开【客户端】
【服务端】开启后,自动进入监听模式,监听是否有【客户端】要加入,如下图
然后开启【客户端】,如下图
此时收到【服务端】发来的一条数据,再看【服务端】界面
至此,简易的TCP客户端与服务端搭建成功!