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;
}