-
-
[原创]网络编程技术学习笔记之基础模型
-
2021-11-22 16:29 19028
-
网络编程技术目录:
一.网络编程基础知识
在Windows上想要实现网络编程首先需要使用WSAStartup来初始化Winsock库,定义如下:
int WSAStartup( __in WORD wVersionRequested, __out LPWSADATA lpWSAData);
参数 | 含义 |
wVersionRequested | 需要使用Winsock版本信息 |
lpWSAData | 指向WSADATA结构体指针,用来接收已初始化的库信息 |
Winsock库有多个版本,要使用的库的版本信息使用第一个WORD类型的参数来进行指定。其中,高8位为副版本号,低8位为主版本号。操作字节不太方便,为了编程方便,通常会使用MAKEWORD宏来构建WORD型版本信息。
MAKEWORD(1, 2):主版本为1,副版本为2,返回0x201
MAKEWORD(2, 2):主版本为2,副版本为2,返回0x202
当程序需要退出的时候,需要使用WSACleanup来注销该库,定义如下:
int WSACleanup(void);
计算机之间的网络通信是通过套接字来实现的,所以无论是客户端还是服务端都需要使用socket函数来创建套接字,该函数定义如下:
SOCKET WSAAPI socket( __in int af, __in int type, __in int protocol);
参数 | 含义 |
af | 套接字中使用的协议族信息 |
type | 套接字数据传输类型信息,决定了套接字传输 |
protocol | 计算机间通信中使用的协议信息 |
af参数指定套接字的协议族的信息,大致分为以下几类,不过最常用的还是PF_INET和PF_INET6。
名称 | 含义 |
PF_INET | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell协议族 |
type参数则指定了套接字之间传输数据的方式,常用的有以下三种:
名称 | 含义 |
SOCK_STREAM | 面向连接的套接字 |
SOCK_DGRAM | 面向消息的套接字 |
SOCK_RAW | 原始协议接口 |
protocol可以选择IPPROTO_TCP,IPPROTO_UDP,IPPROTO_ICMP等协议。
该参数的值由第二个参数决定。
如果第二个参数为SOCK_STREAM,那么该参数就是IPPROTO_TCP,如果第二个参数是SOCK_DGRAM,那么该参数就是IPPROTO_UDP
函数执行成功,则会获得套接字,不再使用该套接字的时候,应当用closesocket来关闭套接字,该函数定义如下:
int closesocket( __in SOCKET s);
有了套接字以后就可以实现计算机之间的网络通信,但对于客户端和服务端来说,它们接下来要做的事情就不同的。
服务端:
对于服务端来说,接下来就需要使用bind函数来给套接字分配IP地址和端口号,该函数定义如下
int bind( __in SOCKET s, __in const struct sockaddr* name, __in int namelen);
参数 | 含义 |
s | 要分配地址信息(IP地址和端口号)的套接字文件描述符 |
name | 指向结构体sockaddr的指针,该结构体保存了地址信息 |
namelen | name结构体变量的大小 |
由此可见,IP地址和端口号是由第二个参数决定的,sockaddr结构体的定义如下:
struct sockaddr {u_short sa_family; char sa_data[14];};
由于该结构体不好赋值,所以Winsock库给我们提供了sockaddr_in结构体,该结构体定义如下:
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
成员 | 含义 |
sin_family | 地址族(必须是AF_INET) |
sin_port | 指定端口 |
sin_addr | 指定IP地址 |
sin_zero | 全为0,用来填充大小足够sockaddr结构体的大小 |
其中指定IP地址的sin_addr是一个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, *PIN_ADDR, FAR *LPIN_ADDR;
可以看到,该结构体中保存了联合体S_un,而S_un中则保存了整型S_addr,该成员就是用来保存IP地址的。由于IP地址的保存是用整型,所以为了编程方便需要使用inet_addr,该函数可以将字符串转为整型,定义如下:
unsigned long inet_addr(__in const char* cp);
由于在网络中字节顺序是按大尾排序,而Windows系统兼容的CPU是小尾模式的,所以在对IP地址和端口进行赋值的时候,需要使用htonl和htons将小尾排序的转为大尾排序,这两个函数定义如下:
u_long WSAAPI htonl( __in u_long hostlong ); u_short WSAAPI htons( __in u_short hostshort );
可以看到这两个函数功能一样,只是参数与返回值大小不一样,htonl是整型,而htons是短整型。
客户端:
对于客户端来说,此时则需要使用connect函数与服务端进行连接,该函数定义如下:
int WSAAPI connect( __in SOCKET s, __in_bcount(namelen) const struct sockaddr FAR * name, __in int namelen );
参数 | 含义 |
s | 用来用服务器通信的套接字 |
name | 与bind函数的name参数含义一样,只不过这里是用来指定要连接的服务器的IP地址与端口 |
namelen | name的大小 |
服务端与客户端完成连接以后,接下来就可以进行通信。其中recv函数用来接收数据,该函数定义如下:
int recv( __in SOCKET s, __out char* buf, __in int len, __in int flags);
参数 | 含义 |
s | 用来通信的套接字 |
buf | 指向要接收的数据的缓冲区指针 |
len | 指定缓冲区长度 |
flags | 影响此函数行为的一组标记 |
send函数用来发送数据,该函数定义如下:
int WSAAPI send( __in SOCKET s, __in_bcount(len) const char FAR * buf, __in int len, __in int flags );
参数 | 含义 |
s | 用来通信的套接字 |
buf | 指向要发送数据的缓冲区的指针 |
len | 缓冲区长度 |
flags | 影响此函数行为的一组标记 |
但要注意的是,这两个函数是对缓冲区进行读数据与发数据的,并不是直接对对方计算机直接操作。也就是说,recv函数是从缓冲区读取数据,真正从对方计算机获取数据的是由操作系统完成,操作系统获取数据以后会将输入放入缓冲区中。同样send函数是向缓冲区中写入数据,真正完成向对方计算机发送数据的也是由操作系统将缓冲区中的数据发送到对方计算机。
这就是网络通信的最基础的一组操作,但是根据数据传输方式的不同,可以选择TCP或者UDP进行通信。TCP/UDP在TCP/UDP协议栈中的位置如下图所示:
也就是说,无论是TCP还是UDP,操作都是在第二层完成的。
二.TCP
要实现基于TCP的传输,首先在创建套接字的时候要创建的是面向连接的套接字(SOCK_STREAM),该套接字有以下三个特点:
传输过程中数据不会消失
按序传输数据
传输数据不存在数据边界
前面说过,发送与接收数据是要通过缓冲区的,那么接收数据的缓冲区已经满了,此时就会停止传输数据,这就保证了数据不会丢失
其次,在TCP传输中,在服务端在调用bind函数绑定地址信息以后,需要调用listen函数来监听端口,该函数定义如下:
int WSAAPI listen( __in SOCKET s, __in int backlog );
参数 | 含义 |
s | 要监听的套接字 |
backlog | 挂起连接队列的最大长度。如果设置为SOMAXCONN,则负责套接字s的底层服务提供商将backlog设置为最大合理值。没有获得实际积压价值的标准规定 |
该函数让套接字处于监听状态,第二个参数决定了该服务器可以发起的连接数有多少,处于监听状态的套接字可以通过accept函数来完成与客户端的连接,该函数的定义如下:
SOCKET accept( __in SOCKET s, __out struct sockaddr* addr, __inout int* addrlen);
参数 | 含义 |
s | 处于监听状态的服务器的套接字 |
addr | 指向接收连接实体地址的缓冲区的可选指针,也就是该指针所指缓冲区用来接收连接服务器的客户端的信息 |
addrlen | addr的长度 |
而对于客户端,只需要在初始化套接字的时候选择TCP方式传输数据的套接字,之后在与服务端连接以后就可以实现客户端与服务端的通信。
以下两张图则总结了TCP方式通信的服务端与客户端指向的步骤
还有一个问题就是连接的关闭,在两台主机通过套接字建立连接后进入可交换数据的状态,又称"流形成的状态"。也就是把建立套接字后可交换数据的状态看作一种流
一旦建立了连接,每个主机都会有单独的输入流与输出流,通过这两条流实现了数据的传输。但是考虑这样的情况,那就是客户端正在给服务端传输数据,可是服务端此时关闭了服务器,这就会造成数据的丢失,为了避免这种状况,就需要使用到shutdown函数,该函数定义如下:
int WSAAPI shutdown( __in SOCKET s, __in int how );
参数 | 含义 |
s | 需要断开连接的套接字 |
how | 传递断开的方式 |
how参数决定了该函数如何断开套接字,不同值得含义如下:
值 | 含义 |
SD_RECEIVE | 断开输入流 |
SD_SEND | 断开输出流 |
SD_BOTH | 同时断开I/O流 |
通过这个函数,就可以实现套接字的半关闭,其目的如下图所示,这就保证了数据的完整
最后给出完整代码。
服务端代码:
// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <WinSock2.h> #include <Windows.h> #pragma comment(lib, "ws2_32.lib") VOID ShowError(PCHAR msg); DWORD WINAPI ThreadProc(LPVOID lpParameter); int main() { WSADATA wsaData; SOCKET hServerSocket = INVALID_SOCKET, hClientSocket = INVALID_SOCKET; SOCKADDR_IN servAddr, clientAddr; int iClientAddrSize = 0; // 初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { ShowError("WSAStartup"); goto fail; } // 创建套接字 hServerSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (hServerSocket == INVALID_SOCKET) { ShowError("socket"); goto exit; } // 绑定端口 memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(1900); if (bind(hServerSocket, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { ShowError("bind"); goto exit; } // 监听端口 if (listen(hServerSocket, SOMAXCONN) == SOCKET_ERROR) { ShowError("listen"); goto exit; } printf("服务端已开启...\n"); while (TRUE) { // 与客户端建立连接 iClientAddrSize = sizeof(clientAddr); memset(&clientAddr, 0, iClientAddrSize); hClientSocket = accept(hServerSocket, (SOCKADDR*)&clientAddr, &iClientAddrSize); if (hClientSocket == INVALID_SOCKET) { ShowError("accept"); goto exit; } // 开起线程与客户端通信 printf("新的连接:ip=%s\n", inet_ntoa(clientAddr.sin_addr)); HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)hClientSocket, 0, NULL); if (hThread == NULL) { printf("CreateThread Error %d\n", GetLastError()); goto exit; } CloseHandle(hThread); } exit: if (hServerSocket != INVALID_SOCKET) { closesocket(hServerSocket); hServerSocket = INVALID_SOCKET; } if (hClientSocket != INVALID_SOCKET) { closesocket(hClientSocket); hClientSocket = INVALID_SOCKET; } WSACleanup(); fail: system("pause"); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParameter) { SOCKET hSocket = (SOCKET)lpParameter; CHAR szMessage[20] = { "Hello 1900" }; if (send(hSocket, szMessage, strlen(szMessage), 0) == SOCKET_ERROR) { ShowError("send"); goto exit; } // 发送数据完毕,断开输出流 shutdown(hSocket, SD_SEND); memset(szMessage, 0, sizeof(szMessage)); // 接收需要得数据 if (recv(hSocket, szMessage, sizeof(szMessage) - 1, 0) == SOCKET_ERROR) { ShowError("send"); goto exit; } exit: if (hSocket) { closesocket(hSocket); } return 0; } VOID ShowError(PCHAR msg) { printf("%s Error %d\n", msg, WSAGetLastError()); }
客户端代码:
// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") VOID ShowError(PCHAR msg); int main() { WSADATA wsaData; SOCKET hSocket = INVALID_SOCKET; SOCKADDR_IN servAddr; // 初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { ShowError("WSAStartup"); goto fail; } // 创建套接字 hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (hSocket == INVALID_SOCKET) { ShowError("socket"); goto exit; } // 连接服务端 memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); servAddr.sin_port = htons(1900); if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { ShowError("connect"); goto exit; } CHAR szBuf[256] = { 0 }; // 接收消息 if (recv(hSocket, szBuf, sizeof(szBuf) - 1, 0) == SOCKET_ERROR) { ShowError("recv"); goto exit; } printf("%s\n", szBuf); // 接收数据完毕,断开输入流 shutdown(hSocket, SD_RECEIVE); if (send(hSocket, szBuf, sizeof(szBuf) - 1, 0) == SOCKET_ERROR) { ShowError("send"); goto exit; } exit: if (hSocket != INVALID_SOCKET) { closesocket(hSocket); hSocket = INVALID_SOCKET; } WSACleanup(); fail: system("pause"); return 0; } VOID ShowError(PCHAR msg) { printf("%s Error %d\n", msg, WSAGetLastError()); }
最终可以看到服务端与客户端之间的成功通信
三.UDP
要想使用UDP通信,创建套接字的时候就要选择使用面向消息的套接字(SOCK_DGRAM),该传输方式有如下的特点:
强调快速传输而非传输顺序
传输的数据可能丢失也可能损毁
传输的数据有数据边界
限制每次传输的数据大小
相比于TCP,UDP无需调用listen和accept函数来建立连接,服务端和客户端可以直接交换数据。在UDP方式通信中,发送数据的函数是sendto,该函数定义如下
int WSAAPI sendto( __in SOCKET s, __in_bcount(len) const char FAR * buf, __in int len, __in int flags, __in_bcount(tolen) const struct sockaddr FAR * to, __in int tolen );
参数 | 含义 |
s | 用于传输数据的套接字 |
buf | 要发送数据的缓冲区地址 |
len | 要发送数据的长度 |
flags | 可选参数,没有则为0 |
to | 存有目标地址信息的sockaddr结构体变量的地址值 |
addrlen | 指向参数to的长度的指针 |
而接收数据则是用recvfrom函数来接收数据
int WSAAPI recvfrom( __in SOCKET s, __out_bcount_part(len, return) __out_data_source(NETWORK) char FAR * buf, __in int len, __in int flags, __out_bcount_part_opt(*fromlen, *fromlen) struct sockaddr FAR * from, __inout_opt int FAR * fromlen );
参数 | 含义 |
s | 用于传输数据的套接字 |
buf | 保存接收数据的缓冲区地址 |
len | 缓冲区地址的长度 |
flags | 可选参数,没有则为0 |
from | 存有发送端地址信息的sockaddr结构体变量的地址值 |
fromlen | 指向参数from的大小指针 |
可以看到,相对于TCP的通信,在发送和接收数据的时候,这两个函数都保存了相应的地址信息。完整的UDP通信方式代码如下:
服务端代码
// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <WinSock2.h> #include <Windows.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 30 VOID ShowError(PCHAR msg); int main() { WSADATA wsaData; SOCKET hServerSocket = INVALID_SOCKET; SOCKADDR_IN servAddr, clientAddr; int iClientAddrSize = 0; char szBuf[BUF_SIZE] = { 0 }; // 初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { ShowError("WSAStartup"); goto fail; } // 创建套接字 hServerSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (hServerSocket == INVALID_SOCKET) { ShowError("socket"); goto exit; } // 绑定端口 memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(1900); if (bind(hServerSocket, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { ShowError("bind"); goto exit; } printf("服务端已开启...\n"); while (TRUE) { memset(szBuf, 0, BUF_SIZE); iClientAddrSize = sizeof(clientAddr); memset(&clientAddr, 0, iClientAddrSize); if (recvfrom(hServerSocket, szBuf, BUF_SIZE, 0, (SOCKADDR *)&clientAddr, &iClientAddrSize) == SOCKET_ERROR) { ShowError("recvfrom"); goto exit; } printf("从IP:%s中收到数据:%s\n", inet_ntoa(clientAddr.sin_addr), szBuf); } exit: if (hServerSocket != INVALID_SOCKET) { closesocket(hServerSocket); hServerSocket = INVALID_SOCKET; } WSACleanup(); fail: system("pause"); return 0; } VOID ShowError(PCHAR msg) { printf("%s Error %d\n", msg, WSAGetLastError()); }
客户端代码
// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib") #define BUF_SIZE 30 VOID ShowError(PCHAR msg); int main() { WSADATA wsaData; SOCKET hSocket = INVALID_SOCKET; SOCKADDR_IN servAddr; // 初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { ShowError("WSAStartup"); goto fail; } // 创建套接字 hSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (hSocket == INVALID_SOCKET) { ShowError("socket"); goto exit; } // 向服务器发送数据 memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); servAddr.sin_port = htons(1900); if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { ShowError("connect"); goto exit; } CHAR szBuf[BUF_SIZE] = { "Hello 1900" }; if (sendto(hSocket, szBuf, BUF_SIZE, 0, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { ShowError("sendto"); goto exit; } printf("数据发送完毕\n"); exit: if (hSocket != INVALID_SOCKET) { closesocket(hSocket); hSocket = INVALID_SOCKET; } WSACleanup(); fail: system("pause"); return 0; } VOID ShowError(PCHAR msg) { printf("%s Error %d\n", msg, WSAGetLastError()); }
程序运行以后成功实现通信