四、Socket网络通信基础(一):简易TCP服务端和客户端

前言

本篇文章目标:解决方案创建 -> 基础项目创建 -> 实现:简易TCP服务端与简易TCP客户端

一、HelloSocket

1、解决方案创建

  • VS2019创建新项目 -> C++,Windows -> 空项目
  • SDL检查 -> 不勾选
  • 项目名称:“HelloSocket”,解决方案名称:“EasyTcpSocket”,位置“D:\test”
  • 修改工程的输出目录和中间目录配置
    • 注意输出目录和中间目录最后面的“/”需要加,否则会报warning
    • 配置属性 -> 常规 -> 所有配置 -> 所有平台
    • 输出目录:$(SolutionDir)../bin/$(Platform)/$(Configuration)/
    • 中间目录:$(SolutionDir)../temp/$(Platform)/$(Configuration)/$(ProjectName)/
  • 添加源代码文件“test.cpp”

2、test.cpp源码

#define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <WinSock2.h>

#pragma comment(lib,"ws2_32.lib")

int main()
{
	//启动Windows socker 2.x环境
	WORD ver = MAKEWORD(2, 2);//创建版本号
	WSADATA dat;
	WSAStartup(ver, &dat);//WinSock的启动函数

	//清除Windows socket环境
	WSACleanup();
	return 0;
}

3、test源码说明

  • #define WIN32_LEAN_AND_MEAN的作用:
    • 尽量避免引入早期的一些库
    • 避免WinSock2.h与Windows.h的冲突
    • 不加载MFC所需的模块,并且提升编译速度
  • “ws2_32.lib”的作用:
    • 提供了网络相关API的支持
    • 如使用WSAStartup、WSACleanup
  • 引入动态链接库“ws2_32.lib”的两种方法
    • 代码方式:#pragma comment(lib,"ws2_32.lib")
    • 属性设置:属性 -> 链接器 -> 输入 -> 附加依赖项 -> 编辑 -> 输入“ws2_32.lib”
  • MAKEWORD(2, 2);含义:
    • 声明调用不同的Winsock版本
    • 例如MAKEWORD(2,2)就是调用2.2版,MAKEWORD(1,1)就是调用1.1版
    • 1.1版只支持TCP/IP协议,而2.0版可以支持多协议
    • winsock 2.0支持异步,1.1不支持异步
  • WSAStartup函数:
    • 使用Socket的程序在使用Socket之前必须调用WSAStartup函数
    • 以后应用程序就可以调用所请求的Socket库中的其它Socket函数了
    • 返回值被忽略: “WSAStartup”:这个返回值不需要,这个永远都是返回0
    • 如果在WSAStartup函数第一个参数中设置的版本号不存在,那么会自动使用WinSock库中最低的版本1.1
  • WSACleanup函数:
    • 应用程序在完成对请求的Socket库的使用后
    • 要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源

二、Socket API函数原型

1、socket

SOCKET WSAAPI socket(
  [in] int af,
  [in] int type,
  [in] int protocol
);

/*
[in] af:地址族规范
当前支持的值为 AF_INET 或 AF_INET6,它们是 IPv4 和 IPv6 的 Internet 地址族格式

[in] type:新套接字的类型规范。
套接字类型的可能值在Winsock2.h头文件中定义
SOCK_STREAM:一种套接字类型,通过 OOB 数据传输机制提供有序的、可靠的、双向的、基于连接的字节流。
此套接字类型使用 Internet 地址族(AF_INET 或 AF_INET6)的传输控制协议 (TCP)。

[in] protocol:要使用的协议。协议参数的可能选项特定于指定的地址族和套接字类型。
IPPROTO_TCP:传输控制协议 (TCP)。当af参数为AF_INET或AF_INET6并且类型参数为SOCK_STREAM时,这是一个可能的值。

返回值:SOCKET
如果没有发生错误, 套接字将返回一个引用新套接字的描述符。否则,返回一个 INVALID_SOCKET 值
*/

2、sockaddr_in

typedef struct sockaddr_in {

#if(_WIN32_WINNT < 0x0600)
    short   sin_family;
#else //(_WIN32_WINNT < 0x0600)
    ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)

    USHORT sin_port;
    IN_ADDR sin_addr;
    CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

/*
sin_family:传输地址的地址族。此成员应始终设置为 AF_INET。
sin_port:传输协议端口号。
sin_addr:包含 IPv4 传输地址的 IN_ADDR结构。
sin_zero:保留供系统使用。WSK 应用程序应将此数组的内容设置为零。
*/

3、htons

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
u_short
WSAAPI
htons(
    _In_ u_short hostshort
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*htons函数采用一个16位数字在主机字节顺序并返回在TCP/IP网络中
使用的网络字节顺序的16位数字(AF_INET或AF_INET6地址族)*/

4、IN_ADDR

typedef struct in_addr {
        union {
                struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { USHORT s_w1,s_w2; } S_un_w;
                ULONG S_addr;
        } S_un;
#define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2    // host on imp
#define s_net   S_un.S_un_b.s_b1    // network
#define s_imp   S_un.S_un_w.s_w2    // imp
#define s_impno S_un.S_un_b.s_b4    // imp #
#define s_lh    S_un.S_un_b.s_b3    // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
/*该组in_addr结构表示IPv4地址*/

5、bind

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
bind(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:标识未绑定套接字的描述符。
[in] name:指向要分配给绑定套接字的本地地址的sockaddr结构的指针。
[in] namelen:name参数指向的值的长度(以字节为单位)。
返回值:如果没有发生错误, bind返回零。否则,它返回 SOCKET_ERROR
*/

6、listen

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
listen(
    _In_ SOCKET s,
    _In_ int backlog
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:标识绑定的未连接套接字的描述符。
[in] backlog:待处理连接队列的最大长度。
    如果设置为SOMAXCONN,负责 socket的底层服务提供者会将 backlog 设置为一个最大的合理值。
返回值:如果没有发生错误, listen返回零。否则,返回SOCKET_ERROR的值
*/

7、accept

accept这个运行完,程序会阻塞,等待连接,才会继续往下执行
一旦有新客户端连接,就会开始执行if (INVALID_SOCKET == sock_client)

		sock_client = accept(sock_server, (sockaddr*)&clientAddr, &nAddrLen);
		if (INVALID_SOCKET == sock_client)
		{
			printf("错误,接受到无效客户端SOCKET...\n");
			closesocket(sock_server);
			WSACleanup();
			return -1;
		}
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
_Must_inspect_result_
SOCKET
WSAAPI
accept(
    _In_ SOCKET s,
    _Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
    _Inout_opt_ int FAR * addrlen
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:一个描述符,用于标识已使用侦听功能置于侦听状态的套接字 。
    该连接实际上是通过accept返回的套接字建立的。
[out] addr:一个可选的指向缓冲区的指针,该缓冲区接收连接实体的地址,如通信层所知。
    addr参数的确切格式由创建sockaddr结构中的套接字时建立的地址族确定。
[in, out] addrlen:一个指向整数的可选指针,该整数包含由addr参数指向的结构的长度。
返回值:如果没有发生错误, accept返回一个SOCKET类型的值,它是新套接字的描述符。
        此返回值是进行实际连接的套接字的句柄。
        否则,返回一个INVALID_SOCKET值
*/

8、send

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
send(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:标识已连接套接字的描述符。
[in] buf:指向包含要传输的数据的缓冲区的指针。
[in] len:buf参数指向的缓冲区中数据的长度(以字节为单位)。
[in] flags:一组指定调用方式的标志。(这个一般填0即可)
返回值:如果没有发生错误, send返回发送的总字节数,可以小于len参数中请求发送的字节数。
        否则,返回 SOCKET_ERROR 的值
*/

9、inet_ntoa

这个函数使用需要:#define _WINSOCK_DEPRECATED_NO_WARNINGS来消除错误
原因是这个有的新的API,使用旧的编译器会报错误

#if INCL_WINSOCK_API_PROTOTYPES
_WINSOCK_DEPRECATED_BY("inet_ntop() or InetNtop()")
WINSOCK_API_LINKAGE
char FAR *
WSAAPI
inet_ntoa(
    _In_ struct in_addr in
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
NET_NTOA功能的转换(IPv4)Internet上的网络地址转换成Internet标准的ASCII字符串点分十进制格式。
返回值:如果没有发生错误, inet_ntoa返回一个字符指针,指向包含标准“.”符号的文本地址的静态缓冲区。
        否则,它返回NULL
*/

10、connect

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
connect(
    _In_ SOCKET s,
    _In_reads_bytes_(namelen) const struct sockaddr FAR * name,
    _In_ int namelen
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:标识未连接套接字的描述符。
[in] name:指向应建立连接的sockaddr结构的指针 。
[in] namelen:name参数指向的sockaddr结构的长度(以字节为单位)。
返回值:如果没有发生错误, connect返回零。否则,它返回 SOCKET_ERROR
*/

11、recv

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
recv(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

/*
[in] s:标识已连接套接字的描述符。
[out] buf:指向接收传入数据的缓冲区的指针。
[in] len:buf参数指向的缓冲区的长度(以字节为单位)。
[in] flags:一组影响此函数行为的标志。(这个一般填0即可)
返回值:如果没有错误发生, recv返回接收到的字节数,buf参数指向的缓冲区将包含接收到的数据。
        如果连接已正常关闭,则返回值为零。
        否则,返回 SOCKET_ERROR 的值
*/

三、简易TCP实现步骤

1、用Socket API建立简易TCP服务端步骤

  • 建立一个socket
  • 绑定接受客户端连接的端口bind
  • 监听网络端口listen
  • 等待接受客户端连接accept
  • 向客户端发送一条数据send
  • 关闭socket,closetsocket

2、用Socket API建立简易TCP客户端步骤

  • 建立一个socket
  • 连接服务器connect
  • 接收服务器消息recv
  • 关闭socket,closetsocket

四、阻塞分析

1、server阻塞

  • server运行后,程序从建立socket -> bind端口 -> listen端口 -> 执行完accep
  • 这时候server进入阻塞,等待客户端连接
  • sock_client = accept(sock_server, (sockaddr*)&clientAddr, &nAddrLen);
  • 直到有新的客户端连接进来后,才会继续往下执行代码

2、client阻塞

  • client运行后,程序从建立socket -> connect服务器 -> 执行完recv
  • 这时候client进入阻塞,等待接收新的消息
  • int nLen = recv(sock_client, recvBuf, 256, 0);
  • 直到client收到新的消息,才会继续往下执行代码

五、完整源码

1、服务端server.cpp

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <Windows.h>
#include <WinSock2.h>
#include<stdio.h>

#pragma comment(lib,"ws2_32.lib")


int main()
{
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	SOCKET sock_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == sock_server)
	{
		printf("socket 创建失败\n");
		WSACleanup();
		return -1;
	}

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	_sin.sin_addr.S_un.S_addr = INADDR_ANY;// INADDR_ANY:这个代表本机的地址都可以访问,如IPV4,IPV6
	//_sin.sin_addr.s_addr = inet_addr("127.0.0.1");// 指定地址的绑定

	if (SOCKET_ERROR == bind(sock_server, (sockaddr*)&_sin, sizeof(_sin)))
	{
		printf("ERROR,绑定网络端口失败...\n");
		closesocket(sock_server);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("绑定网络端口成功...\n");
	}
	if (SOCKET_ERROR == listen(sock_server, 5))
	{
		printf("ERROR,监听网络端口失败...\n");
		closesocket(sock_server);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("监听网络端口成功...\n");
	}

	sockaddr_in clientAddr = {};
	int nAddrLen = sizeof(clientAddr);
	SOCKET sock_client = INVALID_SOCKET;
	char msgBuf[] = "Hello, I'm Server.";

	while (true)
	{
		sock_client = accept(sock_server, (sockaddr*)&clientAddr, &nAddrLen);
		if (INVALID_SOCKET == sock_client)
		{
			printf("错误,接受到无效客户端SOCKET...\n");
			closesocket(sock_server);
			WSACleanup();
			return -1;
		}

		printf("新客户端加入:IP = %s \n", inet_ntoa(clientAddr.sin_addr));
		send(sock_client, msgBuf, strlen(msgBuf) + 1, 0);
	}

	closesocket(sock_server);
	WSACleanup();
	return 0;
}

2、客户端client.cpp

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <Windows.h>
#include <WinSock2.h>
#include<stdio.h>

#pragma comment(lib,"ws2_32.lib")


int main()
{
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	SOCKET sock_client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == sock_client)
	{
		printf("socket 创建失败\n");
		WSACleanup();
		return -1;
	}

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	_sin.sin_addr.s_addr = inet_addr("127.0.0.1");

 	int ret = connect(sock_client, (sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret)
	{
		printf("错误,连接服务器失败...\n");
		closesocket(sock_client);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("连接服务器成功...\n");
	}

	char recvBuf[256] = {};
	int nLen = recv(sock_client, recvBuf, 256, 0);
	if (nLen > 0)
	{
		printf("接收到数据:%s\n", recvBuf);
	}

	closesocket(sock_client);
	WSACleanup();
	getchar();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休止符

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值