使用#pragma命令,在编译时加载:
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,以指明 WinSock 规范的版本,它的原型为:
/** \brief 使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,
* 以指明 WinSock 规范的版本
*
* \param wVersionRequested 指明程序请求使用的Socket版本,
* 其中高位字节指明副版本、低位字节指明主版本
* \param lpWSAData 指向 WSAData 结构体的指针
* \return 返回 0 则成功,否则返回错误代码。
*
*/
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData)
通过MAKEWORD
函数来设置版本好
MAKEWORD(1, 2); //主版本号为 1,副版本号为 2,返回 0x0201
MAKEWORD(2, 2); //主版本号为 2,副版本号为 2,返回 0x0202
WSAData 结构体
typedef struct WSAData {
WORD wVersion; //ws2_32.dll 建议我们使用的版本号
WORD wHighVersion; //ws2_32.dll 支持的最高版本号
//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的实现以及厂商信息
char szDescription[WSADESCRIPTION_LEN+1];
//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的状态以及配置信息
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets; //2.0以后不再使用
unsigned short iMaxUdpDg; //2.0以后不再使用
char FAR *lpVendorInfo; //2.0以后不再使用
} WSADATA, *LPWSADATA;
获取版本号与配置信息
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
typedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;
int main()
{
/* 初始化DLL */
WSADATA wsaData;
//版本为 2.2
WSAStartup(MAKEWORD(2, 2), &wsaData);
//低字节为主版本
printf("wVersion: %d.%d\n", LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));
printf("wHighVersion: %d.%d\n", LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
printf("szDescription: %s\n", wsaData.szDescription);
printf("szSystemStatus: %s\n", wsaData.szSystemStatus);
return 0;
}
/* 输出结构
wVersion: 2.2
wHighVersion: 2.2
szDescription: WinSock 2.0
szSystemStatus: Running
*/
在工程下添加ws2_32.lib
Visual Studio实现socket通信
- 错误 C4996 ‘inet_addr’: Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API
解决方法 - unknown type name ‘sockaddr_in’ 显示未定义改类型
原因有两个,其一是未添加ws2_32.lib库,可以通过手动在工程的link设置里添加,其二是头文件中只定义了该结构名称,但是没有定义它的别名,所以不能直接用sockaddr_in来定义类型,而需要用struct sockaddr_in定义,也可以添加别名定义 typedef struct sockaddr_in sockaddr_in;这样就能运行了。
typedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;
套接字socket
/** \brief 建立一个socket通信
*
* \param af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。
AF 是“Address Family”的简写,
* INET是“Inetnet”的简写。
AF_INET 表示 IPv4 地址
* \param type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM,
* SOCKE_STREAM指定产生流式套接字, SOCK_DGRAM产生数据报套接字
* \param otocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,
* 分别表示 TCP 传输协议和 UDP 传输协议。
* \return 成功时返回一个小的非负整数值,他与文件描述符类似,我们称为套接字描述符,简称sockfd。
* 否则,将返回 INVALID_SOCKET 的值,并且可以通过调用`WSAGetLastError`检索特定的错误代码。
*
*/
SOCKET socket(int af, int type, int protocol)
SOCKET的定义为typedef UINT_PTR SOCKET;
而UINT_PTR
的定义为unsigned long long
。
//创建TCP套接字
SOCKET servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
//创建UDP套接字
SOCKET servSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
SOCKET servSock = socket(AF_INET, SOCK_DGRAM, 0);
上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议。
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;
而客户端要用 connect() 函数建立连接。
/** \brief 对socket定位
*
* \param sock 为 socket 文件描述符
* \param addr 为指向 sockaddr 结构体的指针
* \param addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
* \return 成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
int bind(SOCKET sock, const struct sockaddr* addr, int addrlen)
sockaddr_in结构体
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
其中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;
} IN_ADDR;
例如:将创建的套接字与IP地址 127.0.0.1、端口 1234 绑定:
/* bind */
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = AF_INET; //使用IPv4地址
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
connect() 函数有一些端口号也被正式注册。
它们分布在1024到49151的数字之间,这些端口号可用于任何通信用途。
发送数据需要按照网络序发送数据,故需要调用inet_addr
和htons
/** \brief 建立socket连线
*
* \param sock 为 socket 文件描述符
* \param addr 为指向 sockaddr 结构体的指针
* \param addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
* \return 成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
int connect(SOCKET sock, const struct sockaddr* serv_addr, int addrlen)
listen()和accept()函数
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
/** \brief 等待连接
*
* \param sock 为需要进入监听状态的套接字
* \param backlog 为请求队列的最大长度。
* 我们一般填写这个参数为SOMAXCONN,让系统自动选择最合适的个数
* \return 成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
int listen(SOCKET sock, int backlog)
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
accept() 函数注意:listen() 只是让套接字处于监听状态,并没有接收请求。
接收请求需要使用 accept() 函数。
/** \brief 接受socket连线
*
* \param sock 为 socket 文件描述符
* \param addr 为指向 sockaddr 结构体的指针
* \param addrlen 为 scokaddr 的结构长度
* \return 成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
SOCKET accept(SOCKET sock, struct sockaddr* addr, int* addrlen)
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,注意区分。
后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
socket数据的发送和接收accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
发送数据使用 send() 函数,它的原型为:
/** \brief 经socket传送数据
*
* \param sock 为要发送数据的套接字
* \param buf 为要发送的数据的缓冲区地址
* \param len 为要发送的数据的字节数
* \param flags 为发送数据时的选项。
一般置为0;
* MSG_OOB:传输一段数据,再外带一个额外的特殊数据,但不建议使用,一般忽略就行 ;
* MSG_DONTROUTE:指定数据不应受路由限制,windows套接字服务提供。
程序可以选择忽略
* \return 成功返回写入的字节数,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
int send(SOCKET sock, const char* buf, int len, int flags)
send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。
一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
接收数据使用 recv() 函数,它的原型为:
/** \brief 经socket接收数据
*
* \param sock 接收指定的 socket 传来的数据
* \param buf 为要接收的缓冲区地址
* \param len 可接收数据的最大长度
* \param 参数 flags 一般设0
* \return 成功执行时,返回接收到的字节数
* 另一端已关闭则返回0,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
*
*/
int recv(SOCKET sock, char* buf, int len, int flags)
输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
阻塞模式
对于TCP套接字(默认情况下),当使用 send() 发送数据时:
-
首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
-
如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
-
如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
-
直到所有数据被写入缓冲区 write()/send() 才能返回。
当使用 recv() 读取数据时:
-
首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来,输入缓冲区有数据。
-
如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。
-
直到读取到数据后 recv() 函数才会返回,否则就一直被阻塞。
断开连接注意:TCP的粘包问题以及数据的无边界性
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。
也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
int shutdown(SOCKET s, int howto);
howto 在 Windows 下有以下取值:
- SD_RECEIVE:关闭接收 *** 作,也就是断开输入流。
- SD_SEND:关闭发送 *** 作,也就是断开输出流。
- SD_BOTH:同时关闭接收和发送 *** 作。
确切地说,closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字。
应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的 *** 作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 ,套接字依然存在,直到调用 closesocket() 将套接字从内存清除。
调用 closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。
FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
typedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;
int main()
{
/* 初始化DLL */
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
/* 创建套接字 */
//创建TCP套接字
SOCKET servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/* 绑定套接字 */
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = AF_INET; //使用IPv4地址
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
/* 进入监听状态 */
listen(servSock, SOMAXCONN);
/* 接收客户端请求 */
sockaddr_in clientSockAddr;
unsigned length = sizeof(clientSockAddr);
SOCKET clientSock = accept(servSock, (SOCKADDR*) & clientSockAddr, &length);
/* 向客户端发送数据 */
char *str = "Hello World!";
send(clientSock, str, strlen(str) + 1, 0);
/* 关闭套接字 */
closesocket(clientSock);
closesocket(servSock);
/* 终止 DLL 的使用 */
WSACleanup();
return 0;
}
客户端代码
#include
#include
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
typedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;
int main()
{
/* 初始化DLL */
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
/* 创建套接字 */
//创建TCP套接字
SOCKET clientSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/* 向服务端发送请求 */
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = AF_INET; //使用IPv4地址
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //具体的IP地址
sockAddr.sin_port = htons(1234); //端口
connect(clientSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
/* 向服务端发送数据 */
char *str = "Hello World!";
send(clientSock, str, strlen(str) + 1, 0);
char buff[100] = { 0 };
recv(clientSock, buff, 10, 0);
printf("接收到数据为%s\n", buff);
/* 关闭套接字 */
closesocket(clientSock);
/* 终止 DLL 的使用 */
WSACleanup();
return 0;
}
参考资料
socket、connect、bind函数详解
WSAStartup百度百科
C/C++ socket编程教程:1天玩转socket通信技术
Visual Studio 2019 C++实现socket通信,添加ws2_32.lib库
C语言socket编程
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)