网络编程技术
c/s简易模型–windows网络编程c++(未使用select)
1.比如qq,DNF,LOL等这些我们下载客户端的,都属于c/s模型的一个应用
2.c/s模型其实是概念层面的。实现层面可以是基于任何的网络协议
3.常见的还有b/s模型---浏览器/服务器模型 基于http/https协议的
要展示的效果:
1:局域网
2.广域网可通过内网穿透,内网转发实现
1:sever服务器端分为以下7个步骤
- 打开网络库
- 校验版本
- 创建SOKCKET
- 绑定地质与端口
- 开始监听
- 接受客户端链接(创建客户端socket)
- 与客户端发送信息
1.0头文件
vs2019环境下 需注意scanf报错
解决方法:
1:添加宏定义#define _CRT_SECURE_NO_WARNINGS
2.改为scanf_s
代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>
#include <Ws2tcpip.h>//调用inet_pton()函数替换inet_addr
#include<WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")//库版本为2.2
1.1打开网络库----WSASstartup()
WSAStartup
目前网络库版本:
1.1.0
2.1.1
3.2.0
4.2.1
5.2.2
不管是64编译环境还是32编译环境都用这个,不管是第一版的winsock.h,还是第二版的winsock2.h,都用ws2_32.lib
int WSAStartup(
WORD wVersionRequired,
[out] LPWSADATA lpWSAData
);
WSAS四个字符:
w windows
s socket
a Asynchronous 异步
s startup 启动
类型 | 参数 | 作用 |
---|---|---|
WORD 转定义unsigned short | wVersionRequired | 需要的网络库版本号此处用2.2版本 |
[out] lpWSAData | 指向WSADATA数据结构的指针 ,用于接收 Windows 套接字实现的详细信息。 |
参数2:
1.注意:当看到参数有LP P前缀是,传对应类型变量地址,这是win32API规范
typedef struct WSAData {
WORD wVersion;//我们要使用的版本
WORD wHighVersion;//系统能提供给我们最高的版本
unsigned short iMaxSockets;//返回可用socket的数量,2版本之后就没用了
unsigned short iMaxUdpDg;//UDP数据包信息的大小,2版本之后就没用了
char *lpVendorInfo;//供应商特定的信息,2版本之后就没用了
char szDescription[WSADESCRIPTION_LEN + 1];
char szSystemStatus[WSASYS_STATUS_LEN + 1];
//上两行:当前库的描述信息,,2版本之后就没用了
} WSADATA;
当输入版本不存在时:得到当前输入板本最大版本(主版本0除外)
返回值 | 作用 | 解决方法 |
---|---|---|
0 | 执行正确 | ---- |
WSASYSNOTREADY 10091 | 底层网络子系统尚未准备好进行网络通信 | 系统配置问题,重启,检查ws2_32库是否存在或者是否在环境配置目录下 |
WSAVERNOTSUPPORTED 10092 | 此特定 Windows 套接字实现不提供请求的 Windows 套接字支持版本。 | 使用版本不支持,更新网络库 |
WSAEINPROGRESS 10036 | 操作当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止 | 重启 |
WSAEPROCLIM 10067 | 已达到 Windows 套接字实现支持的任务数量限制。 | 网络连接达到上限或阻塞,关闭不必要的软件 |
WSAEFAULT 10014 | 该lpWSAData参数不是一个有效的指针 | 程序有误请检查,参数写错 |
代码
//打开网络库
//int WSAStartup(WORD wVersionRequired, LPWSADATA lpWASAData);
WSADATA wdScokMsg;
WORD wdVersion = MAKEWORD(2, 2);//用宏存版本
int nRes = WSAStartup(wdVersion, &wdScokMsg);
if (nRes != 0)
{
switch (nRes)
{
case WSASYSNOTREADY://底层网络子系统尚未准备好进行网络通信,重启检查ws2_32库是否存在或者是否在环境配置目录下
printf("系统配置问题,重启");
break;
case WSAVERNOTSUPPORTED:
printf("解决方案:使用版本不支持,更新网络库");
break;
case WSAEINPROGRESS:
printf("解决方案:重启");//当前函数运行期间,由于某些原因造成阻塞,会返回在这个错误码,其他操作均禁止
break;
case WSAEPROCLIM:
printf("解决方案:网络连接达到上限或阻塞,关闭不必要的软件");
break;
case WSAEFAULT:
printf("解决方案:程序有误请检查,参数写错");
break;
}
return 0;
}
1.2校验版本
#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))
高八位表示副版本号,低八位表示主版本号 (HIBYTE高字节副版本 LOBYTE低字节主版本)
代码
if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))//版本不成功时
{
printf("版本有问题");
WSACleanup();//关闭网络库
return 0;
}
出错需关闭库并结束函数
注意:WSAStartup()与WSACleanup()是成对出现
1.2. 创建SOKCKET
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
SOCKET:
socket将底层复杂的协议体系,执行流程,进行封装,就是一个socket–是我们调用协议进行通信的操作接口,每个客户端,服务器都有一个SOCKET,通信时给谁通信就传递谁的SOCKET
本质:
是一个整数数据类型,但是是唯一的,标识我当前的应用程序,协议特点等信息
参数 | 类型 |
---|---|
int af | 地址 |
int type | 套接字 |
int protocol | 协议 |
参数1:
参数 | 取值 | 含义 |
---|---|---|
AF_INET | 2 | IPV4 |
AF_INET6 | 23 | IPV6 |
AF_BTH | 32 | 蓝牙地址协议 |
AF_IRDA | 26 | 红外数据歇会(lrDA)系列 |
参数2:
参数 | 取值 | 含义 |
---|---|---|
– | – | – |
SOCK_STREAM | 1 | 提供带有OOB数据传输机制的顺序,可靠,双向,基于链接的字节流。此套接字类型使用传输控制协议TCP作为internet地址系列(AF_INET或AF_INET6) |
SOCK_DGRAM | 2 | 一种支持数据报的套接字类型,固定(通常很小)最大长度的无连接,不可靠的缓冲区,使用用户数据报协议(UDP)作为internet地址系列(AF_INET或AF_INET6) |
SOCK_RAW | 3 | 要操作IPV4表头,需在套接字上设IP_HDRINCL套接字选型,要操作IPV6表头,需在套接字上设IPV6_HDRINCL套接字选型, |
SOCK_RDM | 4 | 提供可靠消息数据报,仅在安装了可靠多播协议时才支持此类型值 |
SOCK_SEQPACKET | 5 | 提供基于数据报的伪流数据包 |
参数3
参数 | 数值 | 对应协议 | af参数 | 类型参数 |
---|---|---|---|---|
IPPROTO_TCP | 6 | TCP | SOCK_STREAM | AF_INET或AF_INET6 |
IPPROTO_UDP | 17 | UDP | SOCK_DGRAM | AF_INET或AF_INET6 |
IPPROTO_ICMP | 1 | ICMP | SOCK RAW或未指定 | AF_UNSPEC,AF_INET或AF_INET6 |
IPPROTO_IGMP | 2 | IGMP | SOCK RAW或未指定 | AF_UNSPEC,AF_INET或AF_INET6 |
IPPROTO_RM | 113 | PGM | SOCK_RDM | AF_INET(仅在安装了可靠多播协议时才支持此协议值) |
注意:
- 参数1,2,3三者配套
- 要使用设备支持的协议
- 参数3中可填0,系统会自动帮我们选择协议类型,尽量避免这样写,以免以后的代码升级出BUG。
返回值
如果没有发生错误, 套接字返回句柄。否则,返回一个 INVALID_SOCKET 值,并且可以通过调用WSAGetLastError来检索特定的错误代码 。(使用完毕用CloseSocket(###)关闭句柄)
代码
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)//无效socket失败时关闭网络库情况
{
int a = WSAGetLastError();
WSACleanup();
return 0;
}
1.3.绑定地质与端口----bind()
作用
为socket绑定端口号与具体地址
//ipconfig查看ip地址
//netstat -ano查看被使用所有端口
//netstat -ano|findstr "12345"检查端口是否被占用cmd中
端口0-65535 而0-1023一般为系统保留占用端口
int bind(
[in] SOCKET s,
const sockaddr *addr,
[in] int namelen
);
参数1
绑定实质的地址和端口号
参数2
结构体
- 地址类型
- 装ip地址
- 端口号
struct sockaddr {
ushort sa_family;//2字节
char sa_data[14];//14字节
};
struct sockaddr_in {
short sin_family;//2字节
u_short sin_port;//2字节
struct in_addr sin_addr;//4字节
char sin_zero[8];//8字节
};
示例:
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);//htons宏将整形转换为端口号的无符号整形
inet_pton(AF_INET, "127.0.0.1", &si.sin_addr.S_un.S_addr);
调用inet_aton函数替换inet_addr(不支持旧函数)转换为无符号long
可以把下面的强转为上面的类型
参数3
参数2的类型大小–sizeof(参数2)
返回值
成功返回0
失败返回SOCKET_ERROR(调用WSAGetLastError()获得错误码)
可在vs2019中输入错误码查询错误原因
代码
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);//htons宏将整形转换为端口号的无符号整形
inet_pton(AF_INET, "127.0.0.1", &si.sin_addr.S_un.S_addr);//调用inet_aton函数替换inet_addr(不支持旧函数)转换为无符号long
//int bres=bind(socketServer, (const struct sockaddr*)&si, sizeof(si));检查bind返回值
if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))
{
int a = WSAGetLastError();//取错误码
printf("服务器bind失败错误码为:%d\n", a);
closesocket(socketServer);//释放
WSACleanup();//关闭网络库
return 0;
}
printf("服务器端bind成功\n");
1.4. 开始监听 ----listen()
将套接字置于正在侦听传入链接的状态
int WSAAPI listen(
[in] SOCKET s,
[in] int backlog
);
WSAAPI:调用约定
这个可以忽略,给系统看的,和我们无关
决定三个;
- 函数名字的编译方式
- 参数的入栈顺序
- 函数的调用时间
参数1
服务器socket
参数2
1.意义:挂起连接队列的最大长度(系统创建队列记录暂时不能处理的请求)可手动设置2~20+,
2.一般填写SOMAXCONN----让系统自动选择最合适的个数(不同系统环境不一样)
返回值
成功返回0
失败返回SOCKET_ERROR 具体错误码WSAGetLastError()
释放
代码
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
//(服务器句柄,SOMAXCONN系统自动选择最合适的挂起连接队列的最大长度也可手动设置一般为2-20+)
{
int a = WSAGetLastError();//取错误码
printf("服务器监听失败错误码为:%d\n", a);
closesocket(socketServer);//释放
WSACleanup();//关闭网络库
return 0;
}
printf("服务器端监听成功!\n");
1.5. 接受客户端链接(创建客户端socket)----accept()
作用:
- 允许在套接字上进行传入连接尝试
- 将客户端信息绑定到一个新创建socket,通过返回值返回给客户端
- 一次只能创建一个,多个需调用多次
SOCKET WSAAPI accept(
[in] SOCKET s,
[out] sockaddr *addr,
[in, out] int *addrlen
);
参数1
上面创建的socket 先处于监听状态然后来的连接都有这个管理,我们通过这个句柄取客户端信息
参数2
客户端地址端口信息结构体–传址调用
意义:系统帮我们监视着客户端的动态,记录客户端的信息
参数3
参数2的大小
参数2,3可设置为NULL,即不直接得到客户端的地址,端口号,
可通过
getpeername()通过函数得到客户端信息
getsockname(Socket,(sockaddr*)&addr,&len)//得到本地服务器
返回值
成功返回给客户端包好的socket(于客户端通信就靠这个)
失败返回INVALID_SOCKET(获取错误码)
释放
缺点
- 阻塞,同步
- 多个链接需用循环
代码
struct sockaddr_in clientMsg;
int len = sizeof(clientMsg);
//accept//一次只能创建一个客户端socket,有几个客户端链接就要调用几次
SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);//后两个参数也可设置为NULL表示不直接得到客户端的地址,端口号
if (INVALID_SOCKET == socketClient)
{
int a = WSAGetLastError();//取错误码
printf("获取客户端句柄失败错误码为:%d\n", a);
closesocket(socketServer);//释放
WSACleanup();//关闭网络库
return 0;
}
printf("客户端连接成功!\n");
1.6. 与客户端发送信息
作用:得到指定客户端(参数1)发来的消息
原理:
本质是复制----数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储接收的数据,调用recv()就是通过socket找到缓冲区并将数据复制
int WSAAPI recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags
);
参数1
客户端的socket
参数2
客户端消息的存储空间即字节串,一般1500字节(网络传输的最大单元),协议规定客户端发过来最大1500字节,多种情况最优值
参数3
想要读取的字节个数,一般-1,把\0字符串结尾留出来
参数4
数据的读取方式
-
0----依次读出 读出来就删 可计数多少字节
-
MSG_PEEK----读了不删 读数据不行 无法计数
-
MSG_OOB----传输一段距离在外带一个额外特殊数据 REF 793与RFC 1122协议不兼容 可用两次send与recv代替
-
MSG_WAITALL----当buf字节==参数3才读取
返回值
读出来字节数大小
- 读没了 在recv函数卡住等待客户端发来数据即阻塞,同步
- 客户端下线,端返回0 释放客户端socket
- 执行失败返回SOCKTET_ERROR
注意:closesocket()与WSACLeanup()顺序不可颠倒
未写完过两天写