VisualStudio实现socket通信DLL的加载使用#pragma命令,在编译时加载:#pragmacomment(lib,ws2_32.
Visual Studio实现socket通信
DLL的加载
使用#pragma命令,在编译时加载:
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
使用DLL之前,还需要调用 WSAStartup() 函数进行初始化,以指明 WinSock 规范的版本,它的原型为:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
- wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号);
- lpWSAData 为指向 WSAData 结构体的指针。
- 返回 0 则成功,否则返回错误代码。
MAKEWORD(1, 2);
MAKEWORD(2, 2);
WSAData 结构体
typedef struct WSAData {WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN+1];char szSystemStatus[WSASYS_STATUS_LEN+1];unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR *lpVendorInfo;
} WSADATA, *LPWSADATA;
获取版本号与配置信息
#include
#include
#pragma comment (lib, "ws2_32.lib") typedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;int main()
{WSADATA wsaData;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;
}
在工程下添加ws2_32.lib
常见问题
- 错误 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
SOCKET socket(int af, int type, int protocol);
- af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址
- type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM,SOCKE_STREAM指定产生流式套接字,SOCK_DGRAM产生数据报套接字
- protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
- 成功时返回一个小的非负整数值,他与文件描述符类似,我们称为套接字描述符,简称sockfd。否则,将返回
INVALID_SOCKET
的值,并且可以通过调用WSAGetLastError
检索特定的错误代码。
SOCKET的定义为typedef UINT_PTR SOCKET;
UINT_PTR
为unsigned long long
。
SOCKET servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
SOCKET servSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
SOCKET servSock = socket(AF_INET, SOCK_DGRAM, 0);
上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议。
bind()和connect()函数
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;
而客户端要用 connect() 函数建立连接。
bind() 函数
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
- sock 为 socket 文件描述符,
- addr 为 sockaddr 结构体变量的指针,
- addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
- 返回值:成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。
sockaddr_in结构体
struct sockaddr_in{sa_family_t sin_family; uint16_t sin_port; struct in_addr sin_addr; char sin_zero[8];
};
其中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 绑定:
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = AF_INET;
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234);
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
有一些端口号也被正式注册。它们分布在1024到49151的数字之间,这些端口号可用于任何通信用途。
发送数据需要按照网络序发送数据,故需要调用inet_addr
和htons
connect() 函数
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);
各个参数的说明和 bind() 相同,
连接成功,返回0,否则的话,返回SOCKET_ERROR错误
listen()和accept()函数
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。
listen() 函数
int listen(SOCKET sock, int backlog);
- sock 为需要进入监听状态的套接字
- backlog 为请求队列的最大长度。
我们一般填写这个参数为SOMAXCONN
,让系统自动选择最合适的个数 - 成功返回0,失败返回SOCKET_ERROR
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
accept() 函数
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,注意区分。
后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
socket数据的发送和接收
发送数据使用 send() 函数,它的原型为:
int send(SOCKET sock, const char *buf, int len, int flags);
- sock 为要发送数据的套接字
- buf 为要发送的数据的缓冲区地址,
- len 为要发送的数据的字节数,
- flags 为发送数据时的选项。一般置为0。
其他:MSG_OOB:传输一段数据,再外带一个额外的特殊数据,但不建议使用,一般忽略就行 ;MSG_DONTROUTE:指定数据不应受路由限制,windows套接字服务提供。程序可以选择忽略 - 成功返回写入的字节数,失败返回SOCKET_ERROR:通过函数得到相应的错误码,做出相应处理
send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
接收数据使用 recv() 函数,它的原型为:
int recv(SOCKET sock, char *buf, int len, int flags);
成功则返回接收到的字符数, 失败则返回SOCKET_ERROR。
输入输出缓冲区的默认大小一般都是 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() 才能返回。
当使用 read()/recv() 读取数据时:
-
首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来,输入缓冲区有数据。
-
如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
-
直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
注意:TCP的粘包问题以及数据的无边界性
参考资料
socket、connect、bind函数详解
WSAStartup百度百科
C/C++ socket编程教程:1天玩转socket通信技术
Visual Studio 2019 C++实现socket通信,添加ws2_32.lib库
C语言socket编程