NoteDeep

服务端


网络头文件、网络库:

windows最底层的网络函数, 其他的都是对他们的二次封装, 是通用的。

#include <WinSock2.h> // winsock.h 第一版头文件
#pragma comment(lib, "ws2_32.lib") // wsock32.lib 第一版动态库
WinSock2.h
版本:1.0 1.1 2.0 2.1 2.2
网络库只有32位, 64位和32位都可以使用

打开网络库:

函数
int WSAAPI WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); // windows socket Asynchronous startup 异步启动网络库
同步:阻塞/卡死状态
异步:多个工作同时进行

输入主版本存在, 副版本不存在:打开主版本最高的版本号
输入主版本不存在:打开最高版本号
输入0主版本:打开失败
参数
版本号
结构体指针

返回值:(错误码)
WSASYSNOTREADY
系统配置问题, 重启电脑, 检查ws2_32的工作目录
WSAVERNOTSUPPORTED
要使用的版本不支持
WSAEINPROGRESS
函数执行时间过长, 阻塞
WSAEPROCLIM
达到了windows能够打开的网络库数量(硬件能够打开的最大数量, 计算机的端口数量65536个)
WSAEFAULT
第二个参数写错了

WORD wdVersion = MAKEWORD(2, 2); // 版本号, 高字节:1, 低字节:2
WSADATA wdSockMsg;
int nRes = WSAStartup(wdVersion, &wdSockMsg); // LP/P 开头的是需要地址

if (!nRes)
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("重启电脑或检查网络库!\n");
break;
case WSAVERNOTSUPPORTED:
printf("使用的版本不支持, 请更新网络库\n");
break;
case WSAEINPROGRESS:
printf("请重新启动\n");
break;
case WSAEPROCLIM:
printf("请关闭不必要的软件\n");
break;
case WSAEFAULT:
break;
default:
break;
}
}

校验版本:

通过检查结构体的内容判断当前打开的版本是不是需要的版本
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
// 打开的版本不对
// 清理/关闭网络库, 也可以做其他的提示/操作
WSACleanup();
return 0;
}

关闭网络库:

WSACleanup();

创建socket:

socket
将复杂的协议体系、执行流程封装起来的结果
是我们调用协议进行通信的操作接口
将复杂的协议过程和编程人员分开, 只需要使用socket就可以

函数
SOCKET WSAAPI socket(int af, int type, int protocol);
unsigned int -- SOCKET

应用
每一个网络同信函数都要使用SOCKET
通过SOCKET辨别给谁通信

参数
地址类型:ipv4,ipv6,蓝牙地址,红外数据协会地址系列等
ipv4:4字节地址, 0.0.0.0~255.255.255.255, 无符号int类型的范围
AF_INET
ipv6:16字节地址 128位地址
AF_INET6

套接字类型:描述数据的传递方式
SOCK_STREAM:顺序、可靠、双向、基于连接,TCP
SOCK_DGRAM:固定最大长度的无链接、不可靠的缓冲区, UDP
SOCK_RAW:原始套接字
SOCK_RDM:多播
SOCK_SEQPACKET:
... ...

协议类型:
IPPROTO_TCP:AF_INET/AF_INET6、SOCK_STREAM, 可以填这个协议(也可以写其他的符合的协议)
IPPROTO_UDP:
IPPROTO_ICMP:
IPPROTO_IGMP:
IPPROTO_RM:

TCP/IP:
AF_INET
SOCK_STREAM
IPPROTO_TCP

SOCKET sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

释放socket:在清理网络库之前
closesocket(socketServer);

返回值
INVALID_SOCKET:执行失败, 清理网络库, 结束程序
if (INVALID_SOCKET == sockServer)
{
WSACleanup();
return 0;
}

错误码
获取错误码:工具--错误查找
int errorcode = WSAGetLastError();
执行成功也可以调用该函数, 返回 0

绑定地址端口:

给地址类型绑定具体的地址:端口号和具体地址
地址:ip地址
端口号:机器上对应的软件
每种通信的端口号是唯一的
同一个软件可能占用多个端口号

函数声明
int bind( SOCKET s, const sockaddr *addr, int namelen);

参数
socket:
sockaddr *:
将sockaddr_in强制转换称sockaddr
地址类型、端口号、ip地址、填充字节等

sockaddr类型的大小:

sockaddr_in
sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(27015); // 将int转换位指定的类型, 更规范
si.sin_addr是一个结构体, 包含一个联合, 联合中有三个成员, 也就是可以用三种方式赋值

si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

si.sin_addr.S_un.S_un_b.s_b1 = 192;
si.sin_addr.S_un.S_un_b.s_b2 = 168;
... ...

ip地址
127.0.0.1:本地回环地址, 本地网络测试
192.168.xxx.xxx
cmd
ipcondig

端口号
本质就是一个整数:0~65535
0~1023:系统保留
查看端口号是否被占用
cmd
netstat -ano
netstat -ano|findstr "123456"

返回值
成功:0
失败:SOCKET_ERROR, 具体错误吗通过WSAGetLastError()查找

int bres = bind(sockServer, (const sockaddr *)&si, sizeof(sockaddr_in));
if (SOCKET_ERROR == bres)
{
int a = WSAGetLastError();

closesocket(sockServer);
WSACleanup();
return 0;
}

开始监听:

将套接字之于正在侦听传入连接的状态
相当于启动套接字, 接受客户端的连接信息

参数
服务器端的socket

挂起连接队列的最大长度:系统一次只能处理固定的长度,剩下的暂时放在队列中, 2~20左右, SOMAXCONN:系统自己设定

WSAAPI:调用约定(程序员忽略)
作用:决定:函数名字的编译方式, 参数的入栈顺序, 函数的调用时间

返回值
成功:0
失败:SOCKET_ERROR

if (SOCKET_ERROR == listen(sockServer, SOMAXCONN))
{
int errorcode = WSAGetLastError();

closesocket(sockServer);
WSACleanup();
return 0;
}

创建客户端socket:

将客户端连接信息创建成socket
一次只能创建一个, 有几个客户端就要调用几次

函数
SOCKET WSAAPI accept(SOCKET s, sockaddr *addr, int *addrlen);

参数
服务器socket

填充客户端地址端口信息的结构体(地址)

填充参数二的大小

参数二和参数三可以设置为NULL
之后可以调用getpeername()获得具体信息
SOCKET sockClient = accept(sockServer, NULL, NULL);
getpeername(sockClient, (sockaddr *)&clientMsg, &nLen);

getsockname(socket, (sockaddr *)&clientMsg, &nLen); // 得到本地的链接信息, 第一个参数没什么用
返回值
返回包装好的socket
失败返回:INVALID_SOCKET

sockaddr_in clientMsg;
int len = sizeof(clientMsg);
SOCKET sockClient = accept(sockServer, (sockaddr *)&clientMsg, &len);
if (INVALID_SOCKET == sockClient)
{
int errorcode = WSAGetLastError();

closesocket(sockServer);
WSACleanup();
return 0;
}

accept调试
阻塞、同步
多个链接:加循环, 必须要指定好客户端的数量

与客户端收发消息:

recv

函数
int recv(
SOCKET s,
char *buf,
int len,
int flags
);

作用
得到指定客户端(参数一)发来的消息

原理
协议收到数据, 放到协议缓冲区
recv是通过socket找到缓冲区, 将数据从协议缓冲区复制到代码中

参数
指定客户端的socket

客户端消息的存储空间, 字符数组, 一般1500字节(单次最大传输单元)

要读取的字节个数, 一般是参数二字节数-1, 把'\0'留出来

数据的读取的方式:
0 :读出来之后会删除协议缓冲区中的内容
MSG_PEEK :读出来不删除
MSG_OOB :带外数据, 传输一段数据, 另外附带一个额外的特殊数据(一个字节), 不建议使用
MSG_WAITALL :直到系统缓冲区字节数满足参数三所请求的字节数(大于等于), 才开始读取

返回值
读出来的字节数大小, 如果没有收到数据, 会阻塞

0:客户端下线

执行失败:SOCK_ERROR
错误码:WSAGetLastError()进行相应的处理

char buf[1500] = { 0 };
int res = recv(sockClient, buf, sizeof(buf) - 1, 0);

if (0 == res)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == res)
{
// 出错了
int errorcode = WSAGetLastError();
// 根据实际情况处理
}
else
{
printf("%d:", res);
printf("%s\n", buf);
}

send

函数
int WSAAPI send(
SOCKET s,
const char *buf,
int len,
int flags
);

作用
向目标发送数据

原理
将我们的数据复制到协议缓冲区, 计算机伺机发送出去

参数
目标socket

给对方发送的字节串:
不要超过1500字节:网络最大传输单元
超过1500字节:分多次发出去(效率低一点), 有些协议会把多的数据丢弃

发送字节的个数 1400/1024(去掉各个层的包头、尾)

发送方式:
0
MSG_OOB :带外数据, 额外带一个特殊数据
MSG_DONTROUTE :指定数据不受路由限制, windows套接字可以选择忽略此标志, (可能会走固定的线路)


返回值
成功:写入字节数
失败:SOCKET_ERROR
WSAGetLastError()得到错误码, 根据错误码信息做相应处理:重启、等待、不用理会... ...

res = send(sockClient, "abcd", sizeof("abcd"), 0);

if (SOCKET_ERROR == res)
{
int errorcode = WSAGetLastError();
printf("socketServer send failed\n");
}










评论列表

    服务端