#include
#include
#include
#pragma comment( lib, “ws2_32.lib”) // windows socket 32 动态库 没有64位
// 网络学习: 20191120
// 查看网络最高版本库的方法
// 1、头文件
// 2、WSAStartup
int main()
{
printf(" 网络编程学习:\n ");
// 打开网络库
// int WSAAPI WSAStartup(_In_ WORD wVersionRequested, _Out_ LPW);
WORD wdVersion = MAKEWORD( 2, 2); // 支持的最高版本
WSADATA wdSockMsg;
memset( &wdSockMsg, 0x00, sizeof(wdSockMsg));
// int a = *((char *)&wdVersion); // 第一个地址
// int b = *((char *)&wdVersion + 1); // 第二个地址
// LPWSADATA pSocketMsg = (LPWSADATA)malloc( sizeof(WSADATA) );
// 如果传入大于系统的版本,WSAStartup会返回系统支持的最大版本,但是程序不会报错
// 如果传入小于系统的版本,比如0版本。WSAStartup就会报错;
int iRet = WSAStartup( wdVersion, &wdSockMsg); // windows socket 异步 启动
if ( 0 != iRet ) // 文件出错
{
perror( "WSAStartup" );
switch ( iRet )
{
case WSASYSNOTREADY: // 因为它使用提供网络服务的系统目前无效,WSAStartup 目前不能正常工作。 1、重启电脑 2、查看一下网络库
printf("重启电脑,或者检查网络库");
break;
case WSAVERNOTSUPPORTED: // 不支持请求的 Windows 套接字版本。
printf("检查网络库");
break;
case WSAEINPROGRESS: // 目前正在执行一个阻止性操作。
printf("请重新启动");
break;
case WSAEPROCLIM: // 一个 Windows 套接字操作可能在可以同时使用的应用程序数目上有限制。
printf("请关闭不必要的软件,以为软件运行提供资源");
break;
case WSAEFAULT: // 系统检测到在一个调用中尝试使用指针参数时的无效指针地址。
printf("参数错误");
break;
}
return iRet;
}
// 版本校验
if ( HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2 )
{
// 说明版本不对
// 清理网络库
WSACleanup();
// 提示错误
printf("网络库版本不对\n");
return 0;
}
// 什么是socket?
// socket是将底层复杂的协议体系,执行流程,进行封装,封装完成的结果,就是一个socket
// 也就是说SOCKET是我们调用协议进行通讯 操作接口
// 意义
// 将复杂的协议和我们编程人员分开,我们直接操作一个SOCKET就行,对于底层的协议、过程细节,完全不知道,大大开发了开发人员
// 网络编程难就难在协议本身的复杂性,简单就简单在我们编程人员完成不用考虑
// 本质
// socket就是一种数据类型,转定义看类型 就是一个整型/但是这个数是唯一的 标识着我当前的应用程序、协议特点等信息
// 应用
// 网络通信函数全部使用socket
// 每个客户端有一个socket,服务端有一个socket,通信的时候,就需要这个socket做参数,给谁通信,你要传递谁的socket
// 所以 网络编程,理论层面SOCKET是网络封装的精华,代码层面就是不停的使用SOCKET这个变量,所以又叫SOCKET编程
// 3个参数 第一个参数 地址协议 第二个参数 套接字类型 第三个参数 协议类型
// 第一个参数 地址协议
// AF_INET 2 IPv4 地址 Internet协议版本(IPv4)协议系列。 0.0.0.0-255.255.255.255 点分十进制表示
// 4字节32位表示 个数块不够 就是无无符号int类型的范围0-4294967295
// AF_INET6 23 IPv6 Internet协议版本6地址系列 2001:0.32.38:DFE1:63:FEEB
// 16字节 128位表示 这个是地球没一寸IP
// AF_BTH 32 蓝牙地址系列 如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系统 GB:2D:BC:A9:BC:12
// AF_IRDA 26 红外数据协会(IrDA)地址系列 仅当计算机安装了红外端口和驱动程序的时候,才支持地址系列
// 通讯地址不仅仅只有IP地址
// 第二个参数 套接字类型
// SOCK_STREAM 1 一种套接字的类型,提供具有OOB数据传输机制的顺序,可靠、双向、基于连接字节流。此套接字类型可以传输到控制协议(TCP)作为Interent地址系列(AF_INET或AF_INET 6)
// SOCK_DGRAM 2 一种支持数据的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区,此套接字类型使用用户报文协议(UDP)作为Interent地址系列(AF_INET或AF_INET 6)
// SOCK_RAW 3 一种套接字的类型,提供了允许应用程序操作下一个上层协议的套接字,要操作IPv4标头,必须在套接字上设置IP_HDRINCL套节字选项,要操作IPv6的头,必须在套接字上设置IPV6_HDRINCL套接字选项
// SOCK_RDM 4 一种套接字的类型,提供可靠的数据报,这种类型的一个提示Windows中实用了多播(PGM) 多播协议实现,通常称为可靠的多播节目,仅仅安装了可靠多播协议时才支持多类型值
// SOCK_SEQPACKET 5 一种套接字类型,提供了基于数据的伪装数据包
// 第三个参数 协议类型
// IPPROTO_TCP 传输控制协议(TCP),当af参数为AF_INET或AF_INET6且类型参数为SOCK_DGRAM是这是一个可能值
// IPPROTO_UDP 传输数据报协议(UDP),当af参数为AF_INET或AF_INET6且类型参数为SOCK_DGRAM时,这是一个可能值
// IPPROTO_ICMP Internet控制协议(ICMP),当af参数为AF_UNSPEC,AF_IENT和AF_INET6且类型的参数为SOCK_RAW或为指定时,这是一个可能的值
// IPPROTO_IGMP Internet组管理协议(IGMP),当af参数为AF_UNSPEC,AF_INET或AF_INET6且类型参数为SOCK_RAW或未指定是,这个一个可能的值
// IPPROTO_RM 用于可靠多播PGM协议,当af参数为AF_INET且类型参数为SOCK_RDM时,这是一个可能的值,在针对Windows Vista及更高版本发布的Windows SDK上,此仅在安装了可靠多播协议时才支持次协议值
// 整理下 通过参数3得到一个事,参数1,2,3三者是配套,是一套参数,不是随便填的,及使用不同的协议,要添加配套的参数
// 想要使用一个协议,咱们设备支持才行,比如红外;
// 参数3中,有可能有这个词,所以说一般,参数3可以填0,系统可以帮我们选择参数
// 返回值 成功返回可用的socket 不用就一定使用要销毁套接字 closesocket(socketServer);
// 失败返回 INVALID_SOCKET 关闭网络库 WSACleanup() 可用WSAGetLaterroe() 返回错误码
// 当系统库里面不存在的时候,我们给据提供的源码库和头文件引入来解决
SOCKET socketServer = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ( INVALID_SOCKET == socketServer )
{
int a = WSAGetLastError(); // 支持所有的函数 离最近的一个函数的错误号
// 清理网络库
WSACleanup();
return 0;
}
// bind函数
// 作用 给我们的socket绑定端口号与具体的地址 地址 找到电脑只有一个
// 端口号 每一种的端口号都是唯一的 同一个软件可以占用多个端口
// 参数
// 参数1 上一个函数socket,绑定了协议信息(地址类型,套接字类型,协议类型),咱们bind函数就是绑定实质的地址,端口号
// 参数2 结构体 地址类型,装IP的地址 端口号
// 结构体的类型 sockaddr 参数使用的方法 SOCKADDR_IN sockAddress;
// sockAddress.sin_family = AF_INET;
// sockAddress.sin_addr_s_addr = inet_addr("127.0.0.1");
// sockAddress.sin_port = htons(12345);
// 给参数使用方法 (sockaddr *)&sockAddress强转加到参数2
// 参数2的大小 sizeof(参数2)
//
// 端口号:本质就是一个整数 0 - 65535 IP是我们的机器的地址,端口是我们具体软件的通讯端口,一个软件可以占用多个端口,比如一个软件可以聊天,可以下载,可以看视屏,那么这些不同的通讯内容往往有自己的协议,各自的端口
// IP是公司,端口是各个部门的地址
// 填写那个值呢? 理论上这个值在0-65535都可以 实际介于0-1023,为系统保留占用端口 21端口被分配给FTP(文件传输服务)
// 25端口分配给SMIP(简单邮件传输协议)服务
// 80端口分配给HTTP服务
// 所以 我们不写这个范围的 我们的范围是 1024 - 65535 稍微大一点, 1万多
// 但注意的一点是,端口号是唯一的,比如1234已经被别的软件占用了,那么你在使用1234这个端口号,那么就回你绑定失败,因为已经占用了
// 那大家如何查看自己要用的端口号有没有被占用呢? 打开运行cmd输入netstat -ano 查看被使用的所有端口
// netstat -ano|findstr "12345" 检测我们要使用的端口号你是否被使用 使用了就会显示使用的程序,未被是用就啥也不显示
//
// 127.0.0.1 回送地址 本地回环地址 本地网络测试
// 192.168.xxx.xxx 可以在控制台输入指令 ipconfig就能看到了
// 或者在网络设置中,能找到这个地址
//
// 返回值说明: 成功返回0
// 失败返回SOCKET_ERROR 具体通过 int WSAGetLastError(void)获得
// closesocket(socketServer);
// WSACleanup();
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345); // 端口
si.sin_addr.s_addr = inet_addr("127.0.0.1"); // inet_addr 将点分十进制转换成无符号的long
if ( SOCKET_ERROR == bind( socketServer, (struct sockaddr *)&si, sizeof(struct sockaddr)))
{
// 出错le
int a = WSAGetLastError();
// 释放
closesocket(socketServer);
// 清理网络库
WSACleanup();
return -1;
}
// 开始监听
// int WSAAPI listen(SOCKET s,int backlog);
// 作用 将套接字置于正在监听传入直接连接的状态
// 参数1 服务器的socket,也就是socket函数创建的
// 参数2 挂起连接队列的最大长度
// 就是说,比如100个用户连接请求,但是系统一次只能处理20个,那么剩下来的80个不能不理人家,所以系统就创建记录暂时不能处理,一会儿处理连接请求,依次由先后顺序处理,
// 但这个队列有多大?比如2,那么允许2个人在排队,这个不可能无限大,那内存就不够了
//
// 我们可以手动设置这个参数,但别太大,可能2-10多 -20多
// 我们一般填写这个参数 SOMAXCONN 作用是让系统自动选择最合适的这个数
// 不同的系统函数环境不一样,所以这个合适的数也不一样
// WSAAPI 调用约定 这个我们可以忽略,这是给系统看到,给咋们没有关系
// 决定三 函数名字的编译方式
// 参数的入栈顺序
// 函数的调用时间
// 返回值 成功 返回0
// 失败 返回 SOCKET_ERROR
// 用 WSAGetLastError(); 获取错误码
//
// 释放
// closesocket(socketServer);
// WSACleanup();
if ( SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
// 出错le
int a = WSAGetLastError();
// 释放
closesocket(socketServer);
// 清理网络库
WSACleanup();
return -1;
}
// accept函数
// 函数原型 SOCKET WSAAPI accept( SOCKET s, socketaddr *addr,int *addrlen)
// 作用 accept函数允许在套接字上进入连接尝试
// listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是将给客户端返回一个socket通过返回值返回给客户端的socket
// 一次只能创建一个,有几个客户端链接,就要调用几次
//
// 参数1 我们上面创建的自己的socket socket先处于监听状态,然后来的链接都在由这个管理,我们取客户端的信息,就是通过这个我们自己的socket
// 参数2 struct socketaddr的结构体 客户端的地址 端口的结构体
// 和bind的第二个参数是一样的
// 意义:系统帮我们监听这客户的动态,肯定会记录客户端的信息,也就是IP地址,和端口号,并通过这个结构体记录
// SOCKETADDR_IN socketClient 这个我们不填写,系统帮我们填写,也即传址调用
// 参数2 3 也都可以设置成NULL,那就不直接得到客户端的地址,端口号等
// 对此可以通过函数得到客户端信息 getpeername(newSocket, (struct socketaddr*)&clientMsg, &nLen);
// 得到本地服务器信息 getsockname( sSocket, (sockaddr *)&addr, &nLen);
// 参数3 参数2的大小
// 返回值 成功 返回值就是给客服端的socket 与客户端通讯就靠这个
// 失败 返回INVALID_SOCKET WSAGetLastError()得到的错误码
// 释放空间 closesocket( socketServer );
// WSACleanup();
//
// accept 1、阻塞、同步 这和函数是阻塞的、没有客户端链接,就一直卡在这,等着
// 2、多个链接 一次只能一个,5个就要5次循环
//
// 创建客户端链接
struct sockaddr_in clientMsg;
int len = sizeof(struct sockaddr);
SOCKET socketClient = accept( socketServer, (struct sockaddr *)&clientMsg, &len);
if ( INVALID_SOCKET == socketClient )
{
printf("客户端链接失败");
// 出错le
int a = WSAGetLastError();
// 释放
closesocket(socketServer);
// 清理网络库
WSACleanup();
return -1;
}
printf("客户端链接成功");
// 与客户端收发消息
// 收 int recv(SOCKET s,char *buf,int len,int flag);
// 作用 得到客户端(参数1)发来的消息
// 原理 本质 复制 数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着接收到的数据
// 咋们外面调用recv的作用,就是通过socket找到这个缓冲区,并把数据复制到参数2,复制到参数3
// 参数1 客户端的socket,每一个客户对应唯一的socket
// 参数2 客户端的存储空间,也就是个字符数组 这个一般1500字节 网络传输的最大单元,1500字节。也就是客户端发过来的数据,一次最大就是1500字节,这是协议规定,这个数值也是根据很多情况,
// 总结出的最优值
// 所以客户端最多一组来1500字节,咋们这头1500读一次,够够的
// 参数3 想要读取到字节个数 一般参数2得字节数-1,把0字符串结尾留出来
// 参数4 数据的读取方式 0
// 正常逻辑来说 我们从系统缓冲区把数据读到我们的部分,读到我们buf中后,系统缓冲区的被删除掉,必然浪费空间,毕竟,通讯时间长的话,那就爆炸了
// 我们将缓冲区的数据读到我们自己的buf,根据需要处理相应的数据,这是我们可控的,完全玩弄于咋们自己的鼓掌,系统缓冲区的数据,咋们无可奈何,操作不了
// 读出就删的话,有很多好处 1、系统缓冲区读到的数据,比我们的buf多,那么我们读出,系统删掉,从而我们就可以依次的把所有数据读完 比如 系统缓冲区收到abcdefghl,咋们一次读4个字节,那么在我们的循环里,就每次读出abcdefghl
// 如果不删,那么每次从头读 循环每次都读出出abcd,只读到这四个
// 正常逻辑填0 读完就删
// 2 可以计算收到多少字节 返回值就是本次读出来的数据
//
// MSG_PEEK 窥视传入的数据,数据将复制到缓冲区中,但不会从输入队列中删除
// 这个东西是不是不建议被使用 第一、读数据不行 第二、那就无法计算计数了
//
// MSG_OOB 外带数据 意义:就是传输一段数据,在外带一个额外的特殊数据 相当于小声BB
// 实际就不建议使用 1、TCP协议规范(RFC 793)中OOB的原始描述被“主机要求”规范取代(RFC 1122),但仍有许多机器具有RFC793 OOB实现
// 2、既然另种数据,那咋们就send两次,另一方recv两次就行了,何必搞得那么神神秘秘,浪费计算机精力
// MSG_WAITALL 直到系统缓冲区字节数满足参数3所请求字节数,才开始读取
//
// 返回值 读出字节的大小 在recv函数卡着,等着客户端发来数据 即阻塞,同步,阻塞的
// 客户端下线,这端返回0 释放客户端socket
// 执行失败,返回SOCKER_ERROR WSAGetLastError()的到错误码
// 根据错误码信息做相应的处理 重启/等待/不用理会
char buf[1500] = {0};
int res = recv( socketClient, buf, 1499, 0);
if ( 0 == res ) // 连接中断
{
printf("连接中断");
}
else if ( SOCKET_ERROR == res)
{
// 出错了
int a = WSAGetLastError();
// 根据实际情况进行处理
}
else
{
printf("%d %s\n", res, buf);
}
// send 函数
//
// 函数原型 int WSAAPI send( SOCKET s,const char* buf, int len,int flags);
// 作用 向目标函数发送数据
// 本质 send函数将我们的数据复制粘贴进系统的协议发送缓冲区,计算机司机发出去 最大的传输单元是1500字节
// 参数1 目标socket,每个客户端对应唯一的socket
// 参数2 给对方发送的字符串 这个不要超过1500字节
// 发送时候,协议要进行包装,加上协议信息,也叫协议头,或者包头,咋们在理论部分,会非常详细的介绍协议头,以及功能
// 这个大小不同的协议不一样,链路层14字节,ip头20字节,tcp头20字节,数据尾还有状态确认,加起来也几十个字节,所以实际咋们的数据位,不能写1500个,要留出来,那就1024字节,或者最多1400,就差不多了
// 当然大家这个不一定每次正好这么多字节,比如聊天,一句话就是十个八个的汉字,别多于1400字节最好
// 超过1500系统咋办?
// 系统会分片处理,比如2000字节
// 系统分成两个包 1400+包头==1500 假设包头100字节
// 60 + 包头 == 700
// 分两次发送出去
// 结果:1、系统要给咋们分包在打包,在发送,客户端接收到了,还得拆包,组合数据。从而增加了系统的工作,降低了效率
// 2、有的协议,就把分片后的二包数据直接丢了
//
//
// 参数3 字节个数 1400
// 参数4 写0就行
// 其他 MSG_OOB 意义通recv 就不使用了
// MSG_OOB 外带数据 意义:就是传输一段数据,在外带一个额外的特殊数据 相当于小声BB
// 实际就不建议使用 1、TCP协议规范(RFC 793)中OOB的原始描述被“主机要求”规范取代(RFC 1122),但仍有许多机器具有RFC793 OOB实现
// 2、既然另种数据,那咋们就send两次,另一方recv两次就行了,何必搞得那么神神秘秘,浪费计算机精力
// MSG_DONTROUTE 指定数据不受路由器限制,Windows套节字服务提供程序可以选择忽略此标识
// 返回值 成功返回写入的字节数
// 执行失败,返回SOCKET_ERROR WSAGetError得到错误码
// 根据错误码做相应的处理 重启 等待 不用理会
//
if ( SOCKET_ERROR == send(socketClient, "abcd", sizeof("abcd"), 0) )
{
// 出错了
int a = WSAGetLastError();
// 根据实际情况进行处理
}
closesocket( socketClient );
closesocket( socketServer );
// 清理网络库
WSACleanup();
// free(pSocketMsg );
// pSocketMsg = NULL;
system(“pause”);
return 0;
}