2-3 建立简易TCP服务端、客户端【socket server/client】【socket、bind、listen、accept、send、closesocket】【conect、recv】

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 分开储存在两个变量中,如下:

在这里插入图片描述

参考:sockaddr和sockaddr_in详解

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()等函数时出错则会返回该宏:

参考:关于socket的各种错误码

函数声明:int listen (int sockfd, int backlog);

该函数在bind()之后accept()调用之前调用。第一个参数为已经创建的监听socket, 第二个参数是socket 监听队列最大监听连接数。

参考:Linux socket 编程API listen(SOCKET s, int backlog)

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中accept()函数的理解

对于接收到的客户端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

参考:Socket中send函数的理解

那么为什么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。

https://baike.baidu.com/item/recv%28%29

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客户端与服务端搭建成功!

猜你喜欢

转载自blog.csdn.net/weixin_44484715/article/details/111998953