服务端开发基础

TCP/IP协议与CS模型介绍

Transmission Control Protocol/Internet Protocol

  • 重要性:是今天互联网的基石
  • 概念:tcp/ip协议簇,并不是tcp协议和ip协议的总称,tcp/ip指的是整个网络传输体系,而tcp协议和ip协议就是单单的两个协议
  • 特点:面向链接的可靠的基于字节流的传输层协议

User Datagram Protocol/Internet Protocol

  • 面向非连接的,不可靠的,基于数据报的传输层协议

Client/Server

  • QQ,DNF,LOL等下载的客户端都属于C/S模型的一个应用
  • C/S模型其实是概念层面的,实现层面可以是基于任何的网络歇息

Browser/Server

  • 浏览器/服务器模型
  • 基于http/https协议

服务端代码流程

  • 网络头文件,网络库
  • 打开网络库
  • 检验版本
  • 创建SOCKET
  • 绑定地址与端口
  • 开始监听
  • 创建客户端socket/接受链接
  • 与客户端收发消息

客户端代码流程

  • 网络头文件,网络库
  • 打开网络库
  • 检验版本
  • 创建SOCKET
  • 链接到服务器
  • 与服务器收发消息

网络库与网络头文件

1
2
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

WSAStartup()函数

1
2
3
4
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);

功能

打开网络库/启动网络库,启动了这个库,这个库里的函数/功能才能使用

  • w:windows
  • s:socket
  • a:Asynchrono 异步
  • startup 启动

异步与同步

  • 同步:阻塞/卡死状态
  • 异步:多个工作同时进行

参数一

1
2
# WORD wVersionRequired
WORD wdVersion = MAKEWORD(2,2);
  • 类型是WORD,转定义:unsigned short
  • MAKEWORD(主版本,副版本)
  • wVersionRequired,数据高位(低地址)是副版本。数据低位(高地址)是主版本

参数二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct WSAData {
WORD wVersion;# 我们要使用的版本
WORD wHighVersion;# 系统能提供给我们最高的版本
#ifdef _WIN64
unsigned short iMaxSockets;# 返回可用的socket的数量,2版本之后就没用了
unsigned short iMaxUdpDg;# UDP数据报信息的大小,2版本后就没用了
char FAR * lpVendorInfo;# 供应商特定的信息,2版本后就没用了
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];# 当前库的的描述信息
#else
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
#endif
} WSADATA, FAR * LPWSADATA;
# LPWSADATA lpWSAData
# LPWSADATA = WSADATA*
WSADATA wdSockMsg;

返回值

成功返回值为0

错误返回下述错误码

1
2
3
4
5
#define WSASYSNOTREADY                10091L//底层网络子系统尚未准备好进行网络通信
#define WSAVERNOTSUPPORTED 10092L//此特定Windows套接字实现不提供所请求的Windows套接字支持的版本
#define WSAEPROCLIM 10067L//已达到对Windows套接字实现支持的任务数量的限制
#define WSAEINPROGRESS 10036L//正在阻止Windows Sockets 1.1操作
#define WSAEFAULT 10014L//IPWSAData参数不是有效指针

错误码处理方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int nRes = WSAStartup(wdVersion,&wdSockMsg);
if (nRes != 0) {
switch (nRes) {
case WSASYSNOTREADY:
printf("系统配置问题,重启电脑,检查ws2_32库是否存在,或者是否在环境配置目录下");
break;
case WSAVERNOTSUPPORTED:
printf("要使用的版本不支持,请更新网络库");
break;
case WSAEINPROGRESS:
printf("Windows Sockets实现可能限制同时使用它的应用程序的数量");
break;
case WSAEPROCLIM:
printf("当前函数运行期间,由于某些原因造成阻塞,会返回这个操作码,其他操作均禁止");
break;
case WSAEFAULT:
printf("参数写错了,用户问题");
break;
}
}

检验版本

1
2
3
4
5
6
7
#LOBYTE()取16进制数的最低字节,HIBYTE()取16进制数的最高字节。
if (HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2) {
//说明版本不对
//清理网络库
WSACleanup();
return 0;
}

socket()函数

socket

  • 概念:将底层复杂的协议体系,执行流程,进行了封装,封装完的结果就是一个SOCKET了,SOCKET是我们调用协议进行通信的操作接口
  • 意义:将复杂的协议过程与我们编程人员分开,我们直接操作一个简单的SOCKET就行,对于底层的协议细节,不必知道
  • 本质:就是一种数据类型,是一个整数,但是这个数是唯一的
  • 应用逻辑:网络通信的函数,全部都要使用SOCKET,每个客户端有一个SOCKET,服务器有一个SOCKET,通信的时候,就需要这个SOCKET做参数,给谁通信,就传递谁的SOCKET

参数

1
2
3
4
5
6
SOCKET socket(
_In_ int af,//地址的类型
_In_ int type,//套接字类型
_In_ int protocol//协议的类型
);
SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

地址的类型

1
2
3
4
#AF_INET 2 //ipv4
#AF_INET6 23 //ipv6
#AF_BTH 32 //蓝牙地址系列
#AF_IRDA 26 //红外数据协会(IrDA)地址系列

套接字类型

1
2
3
4
5
#SOCK_STREAM 1//一种套接字类型,提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。此套接字类型使用传输控制协议(TCP)作为Internet地址系列
#SOCK_DFRAM 2//一种支持数据报的套接字类型,他是固定最大长度的无连接,不可靠的缓冲区。此套接字类型使用用户数据报协议(UDP)作为Internet地址系列
#SOCK_RAW 3//一种套接字类型,提供允许应用程序操作下一个上层协议头的原始套接字。要操作IPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。要操作IPv6标头,必须在套接字上设置IPv6_HDRINCL选项
#SOCK_RDM 4//一种套接字类型,提供可靠的消息数据报。这种类型的一个实例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。
#SOCK_SEQPACKET 5//一种套接字类型,提供基于数据报的伪流数据包

协议类型

1
2
3
4
5
IPPROTO_TCP 6//TCP协议
IPPROTO_UDP 17//UDP协议
IPPROTO_ICMP 1//ICMP协议
IPPROTO_IGMP 2//IGMP协议
IPPROTO_RM //用于可靠多播的PGM协议

三个参数都是配套使用的,不同的协议需要填对应的参数

返回值

  • 成功返回可用的socket,不用了就一定要销毁套接字
1
closesocket(socketListen)
  • 失败返回INVALID_SOCKET
1
2
关闭网络库 WSACleanup();
可用WSAGetLasterror()返回错误码

bind()函数

1
2
3
4
5
bind(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);

作用

给我们的socket绑定端口号与具体地址

  • 地址:只有一个
  • 端口号:每一种通信的端口号是唯一的,同一个软件可能占用多个端口号

参数

  • 参数一: 上一个函数床的socket
  • 参数二:结构体{地址类型,装IP地址,端口号}
1
2
3
4
5
6
7
8
9
10
11
12
# 第一种
struct sockaddr {
u_short sa_family; /* address family */
char sa_data[14]; /* up to 14 bytes of direct address */
};
# 第二种
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
  • 参数三:参数2的类型大小,sizeof(参数2)

补充

  • netstat -ano 查看所有使用的端口
  • netstat -ano|findstr “12345”检查要使用的端口号,被使用会返回使用的程序,未被使用不显示

返回值

  • 成功返回0
  • 失败返回SOCKET_ERROR,具体错误码通过int WSAGetLastError()获得,然后关闭网络库
1
2
3
4
5
6
7
8
9
10
11
12
13
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);
si.sin_addr.s_addr = htonl(2130706433);
int bres = bind(socketServer, (const struct sockaddr *)&si, sizeof(si));
if (SOCKET_ERROR == bres) {
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}

listen()函数

1
2
3
4
int WSAAPI listen(
_In_ SOCKET s,
_In_ int backlog
);

作用

将套接字置于正在侦听传入连接的状态

参数

  • 参数一:服务端的socket,也就是socket函数创建的
  • 参数二:挂起连接队列的最大长度

WSAAPI

调用约定

返回值

  • 成功返回0
  • 失败返回SOCKET_ERROR,具体错误码可以用WSAGetLastError()接收,然后释放

accept()函数

1
2
3
4
5
SOCKET WSAAPI accept(
_In_ SOCKET s,
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
_Inout_opt_ int FAR * addrlen
);

作用

  • accept函数允许在套接字上进行传入连接尝试
  • listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket通过返回值返回给我们客户端的socket
  • 一次只能创建一个,有几个客户端链接,就要调用几次

参数

  • 参数一:我们上面创建的自己的socket(服务器socket)
  • 参数二:客户端的地址端口信息结构体
  • 参数三:参数二的大小

返回值

  • 成功:返回值就是客户端包装好的socket
  • 失败:返回INVALID_SOCKET

执行特点

  • 阻塞,同步:这个函数是阻塞的,没有客户端链接,就会一直等待
  • 多个链接:一次只能一个,多个就要多次循环

recv()函数

1
2
3
4
5
6
7
8
int
WSAAPI
recv(
_In_ SOCKET s,
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
_In_ int len,
_In_ int flags
);

作用

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

原理

  • 数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着接收到的数据
  • 调用recv的作用,就是通过socket找到这个缓冲区,并把数据复制到参数2,复制的个数为参数三

参数

  • 参数一:客户端的socket,每个客户端对应唯一的socket
  • 参数二:客户端消息的存储空间,也就是个字符数组,这个一般是1500字节(MTU)
  • 参数三:想要读取的字节个数,一般是参数二字节数-1,把\0字符串结尾留出来
  • 参数四:数据读取方式

返回值

  • 读出来的字节数大小
  • 客户端下线,返回0
  • 执行失败,返回SOCKET_ERROR

send()函数

1
2
3
4
5
6
7
8
int
WSAAPI
send(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags
);

作用

  • 向目标发送和数据
  • send函数将我们的数据复制粘贴进系统的协议发送缓冲区,计算机伺机发出去,最大传输单元为1500字节

参数

  • 参数一:目标的socket,每个客户端对应唯一的socket
  • 参数二:给对方发送的字节串
  • 参数三:字节个数(发送的个数)
  • 参数四:一般写0(正常发送)

关于参数二的补充

  • 不要超过1500字节,发送要进行协议包装,不应超过MTU
  • 超过MTU,系统会分片处理。系统要分包再发送,客户端接收到了需要拆包组合数据,从而增加了系统的工作,降低效率。有的协议,就直接把分片后的二包丢了

返回值

  • 成功返回写入的字节数
  • 执行失败,返回SOCKET_ERROR

connect()函数

1
2
3
4
5
6
7
int
WSAAPI
connect(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);

作用

  • 链接服务器并把服务器信息与服务器socket绑定到一起

参数

  • 参数一:服务器socket
  • 参数二:服务器ip地址端口号结构体
  • 参数三:参数二的结构体大小

返回值

  • 成功返回0
  • 失败返回SOCKET_ERROR

客户端完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define _WINSOCK_DEPRECATED_NO_WARNINGS

int main(void) {
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wdSockMsg;
//LPWSADATA lpw = malloc(sizeof(WSADATA));//WSADATA*
int nRes = WSAStartup(wdVersion, &wdSockMsg);
if (nRes != 0) {
switch (nRes) {
case WSASYSNOTREADY:
printf("系统配置问题,重启电脑,检查ws2_32库是否存在,或者是否在环境配置目录下");
break;
case WSAVERNOTSUPPORTED:
printf("要使用的版本不支持,请更新网络库");
break;
case WSAEINPROGRESS:
printf("Windows Sockets实现可能限制同时使用它的应用程序的数量");
break;
case WSAEPROCLIM:
printf("当前函数运行期间,由于某些原因造成阻塞,会返回这个操作码,其他操作均禁止");
break;
case WSAEFAULT:
printf("参数写错了");
break;
}
return 0;
}
//校验版本
if (HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2) {
//说明版本不对
//清理网络库
WSACleanup();
return 0;
}
//服务器的SOCKET
SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)
{
//清理网络库
WSACleanup();
return 0;
}
//链接服务器
struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;
serverMsg.sin_port = htons(12345);
serverMsg.sin_addr.s_addr = htonl(2130706433);
if (SOCKET_ERROR == connect(socketServer, (const struct sockaddr*)&serverMsg, sizeof(serverMsg))) {

closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
//接收消息
char buf[1500] = { 0 };
int res = recv(socketServer, buf, 1499, 0);
if (res == 0) {
printf("链接中断\\n");
}
else if (SOCKET_ERROR == res) {
int a = WSAGetLastError();
}
else
{
printf("%d %s\\n", res, buf);
}
//发送消息
if (SOCKET_ERROR == send(socketServer, "我是客户端", sizeof("我是客户端"), 0)) {
int a = WSAGetLastError();
}

closesocket(socketServer);
//清理网络库
WSACleanup();
system("pause");
return 0;
}

服务器完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define _WINSOCK_DEPRECATED_NO_WARNINGS

int main(void) {
WORD wdVersion = MAKEWORD(2,2);
WSADATA wdSockMsg;
//LPWSADATA lpw = malloc(sizeof(WSADATA));//WSADATA*
int nRes = WSAStartup(wdVersion,&wdSockMsg);
if (nRes != 0) {
switch (nRes) {
case WSASYSNOTREADY:
printf("系统配置问题,重启电脑,检查ws2_32库是否存在,或者是否在环境配置目录下");
break;
case WSAVERNOTSUPPORTED:
printf("要使用的版本不支持,请更新网络库");
break;
case WSAEINPROGRESS:
printf("Windows Sockets实现可能限制同时使用它的应用程序的数量");
break;
case WSAEPROCLIM:
printf("当前函数运行期间,由于某些原因造成阻塞,会返回这个操作码,其他操作均禁止");
break;
case WSAEFAULT:
printf("参数写错了");
break;
}
return 0;
}
if (HIBYTE(wdSockMsg.wVersion) != 2 || LOBYTE(wdSockMsg.wVersion) != 2) {
//说明版本不对
//清理网络库
WSACleanup();
return 0;
}

SOCKET socketServer = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (INVALID_SOCKET == socketServer)
{
//清理网络库
WSACleanup();
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);
si.sin_addr.s_addr = htonl(2130706433);
int bres = bind(socketServer, (const struct sockaddr *)&si, sizeof(si));
if (SOCKET_ERROR == bres) {
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;

}
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN)) {
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
//创建客户端链接
struct sockaddr_in clientMsg;
int len = sizeof(clientMsg);
SOCKET socketClient = accept(socketServer, (struct sockaddr*)&clientMsg, &len);
if (INVALID_SOCKET == socketClient) {
printf("客户端链接失败\\n");
int a = WSAGetLastError();
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
printf("客户端链接成功\\n");
//发送消息
if (SOCKET_ERROR == send(socketClient, "我是服务器", sizeof("我是服务器"), 0)) {
int a = WSAGetLastError();
return 0;
}

//接受消息
char buf[1500] = { 0 };
int res = recv(socketClient, buf, 1499, 0);
if (res == 0) {
printf("链接中断\\n");
}
else if (SOCKET_ERROR == res) {
int a = WSAGetLastError();
}
else
{
printf("%d %s\\n", res, buf);
}


//释放
closesocket(socketClient);
closesocket(socketServer);
//清理网络库
WSACleanup();
system("pause");
return 0;

}

模型分析

问题所在

  • 由于accept,recv是阻塞的,做其中一件事,另外一件事就做不了
  • 当前模型,不管将来是什么请求,都得先等待
  • 在得带recv是,来链接请求的话就无法处理,链接的时候只能accept,不能recv
  • 如果等待的socket没有发请求,那么服务器就陷入死循环

解决,引出select模型

  • select就是挑选的意思,把请求的套接字筛选出来去处理
  • select就是处理accept与recv阻塞问题的
  • send函数本身也就是阻塞的,属于短阻塞