热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

TCP/IP协议学习笔记(三)Window下socket编程(一)

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


  1. wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号);
  2. lpWSAData 为指向 WSAData 结构体的指针。
  3. 返回 0 则成功,否则返回错误代码。

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.dlltypedef struct sockaddr_in sockaddr_in;
typedef struct WSAData WSAData;
typedef struct sockaddr sockaddr;int main()
{/* 初始化DLL */WSADATA wsaData;//版本为2.2WSAStartup(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

在这里插入图片描述
在这里插入图片描述


常见问题


  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

SOCKET socket(int af, int type, int protocol);


  1. af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址
  2. type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM,SOCKE_STREAM指定产生流式套接字,SOCK_DGRAM产生数据报套接字
  3. protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
  4. 成功时返回一个小的非负整数值,他与文件描述符类似,我们称为套接字描述符,简称sockfd。否则,将返回INVALID_SOCKET 的值,并且可以通过调用WSAGetLastError检索特定的错误代码。

SOCKET的定义为typedef UINT_PTR SOCKET;
UINT_PTRunsigned 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() 函数

int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);


  1. sock 为 socket 文件描述符,
  2. addr 为 sockaddr 结构体变量的指针,
  3. addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
  4. 返回值:成功则返回0 ,否则,它将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError来检索特定的错误代码。

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() 函数

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


  1. sock 为需要进入监听状态的套接字
  2. backlog 为请求队列的最大长度。
    我们一般填写这个参数为SOMAXCONN,让系统自动选择最合适的个数
  3. 成功返回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);


  1. sock 为要发送数据的套接字
  2. buf 为要发送的数据的缓冲区地址,
  3. len 为要发送的数据的字节数,
  4. flags 为发送数据时的选项。一般置为0。
    其他:MSG_OOB:传输一段数据,再外带一个额外的特殊数据,但不建议使用,一般忽略就行 ;MSG_DONTROUTE:指定数据不应受路由限制,windows套接字服务提供。程序可以选择忽略
  5. 成功返回写入的字节数,失败返回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() 发送数据时:


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

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

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

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

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


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

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

  3. 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。


注意:TCP的粘包问题以及数据的无边界性



参考资料

socket、connect、bind函数详解

WSAStartup百度百科

C/C++ socket编程教程:1天玩转socket通信技术

Visual Studio 2019 C++实现socket通信,添加ws2_32.lib库

C语言socket编程


推荐阅读
  • POCOCLibraies属于功能广泛、轻量级别的开源框架库,它拥有媲美Boost库的功能以及较小的体积广泛应用在物联网平台、工业自动化等领域。POCOCLibrai ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • 使用C++编写程序实现增加或删除桌面的右键列表项
    本文介绍了使用C++编写程序实现增加或删除桌面的右键列表项的方法。首先通过操作注册表来实现增加或删除右键列表项的目的,然后使用管理注册表的函数来编写程序。文章详细介绍了使用的五种函数:RegCreateKey、RegSetValueEx、RegOpenKeyEx、RegDeleteKey和RegCloseKey,并给出了增加一项的函数写法。通过本文的方法,可以方便地自定义桌面的右键列表项。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 【爬虫】关于企业信用信息公示系统加速乐最新反爬虫机制
    ( ̄▽ ̄)~又得半夜修仙了,作为一个爬虫小白,花了3天时间写好的程序,才跑了一个月目标网站就更新了,是有点悲催,还是要只有一天的时间重构。升级后网站的层次结构并没有太多变化,表面上 ... [详细]
  • 学习笔记17:Opencv处理调整图片亮度和对比度
    一、理论基础在数学中我们学过线性理论,在图像亮度和对比度调节中同样适用,看下面这个公式:在图像像素中其中:参数f(x)表示源图像像素。参数g(x)表示输出图像像素。 ... [详细]
  • 部分转载自:http:blog.csdn.netliujiuxiaoshitouarticledetails69920917头文件#include<assert.h& ... [详细]
  • 如何基于ggplot2构建相关系数矩阵热图以及一个友情故事
    本文介绍了如何在rstudio中安装ggplot2,并使用ggplot2构建相关系数矩阵热图。同时,通过一个友情故事,讲述了真爱难觅的故事背后的数据量化和皮尔逊相关系数的概念。故事中的小伙伴们在本科时参加各种考试,其中有些沉迷网络游戏,有些热爱体育,通过他们的故事,展示了不同兴趣和特长对学习和成绩的影响。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • SQL Server 2008 到底需要使用哪些端口?
    SQLServer2008到底需要使用哪些端口?-下面就来介绍下SQLServer2008中使用的端口有哪些:  首先,最常用最常见的就是1433端口。这个是数据库引擎的端口,如果 ... [详细]
author-avatar
巴黎来的
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有