TCPIP协议学习笔记(三)Window下socket编程

TCPIP协议学习笔记(三)Window下socket编程,第1张

DLL的加载

使用#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通信

常见问题
  1. 错误 C4996 ‘inet_addr’: Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API
    解决方法
  2. 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,系统会自动推演出应该使用什么协议。


bind()和connect()函数

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;
而客户端要用 connect() 函数建立连接。


bind() 函数
/** \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));

有一些端口号也被正式注册。


它们分布在1024到49151的数字之间,这些端口号可用于任何通信用途。



发送数据需要按照网络序发送数据,故需要调用inet_addrhtons

connect() 函数
/** \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() 函数,就可以随时响应客户端的请求了。


listen() 函数
/** \brief	等待连接
 *
 * \param   sock 为需要进入监听状态的套接字
 * \param   backlog 为请求队列的最大长度。


* 我们一般填写这个参数为SOMAXCONN,让系统自动选择最合适的个数 * \return 成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。


* */ int listen(SOCKET sock, int backlog)

所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。


注意:listen() 只是让套接字处于监听状态,并没有接收请求。


接收请求需要使用 accept() 函数。


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 是服务器端的套接字,注意区分。



后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。


accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。


socket数据的发送和接收

发送数据使用 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() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。


  2. 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。


  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。


  4. 直到所有数据被写入缓冲区 write()/send() 才能返回。


当使用 recv() 读取数据时:

  1. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来,输入缓冲区有数据。


  2. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。


  3. 直到读取到数据后 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编程

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/584961.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存