文章目录
- 网络的初始化
- 端口的分配
- 协议的使用
- WSAStartup函数
- 参数1:`WORD wVersionRequested`
- 参数2:LPWSADATA lpWSAData
- 返回值 int
- 代码实现
- 创建套接字
- int af表示套接字的类型(网络套接字 文件套接字)
- int type数据流 数据报
- int protocol 通信协议 TCP & UDP
- 返回值
- 代码
- 优化
- 设置端口套接字可复用特性(端口复用)
- 绑定套接字和网络地址
- 动态分配一个端口
- 创建监听队列
- 创建 客户端的访问
- 使用多线程进行用户的访问
- 使用CreateThread函数创建线程
- Http请求的流程
- 第一行报文详细说明
- 响应报文的格式
- POST请求报文的格式
- 报文的分析
网络的初始化
这是网络通信需要包含的头文件以及库文件
#include <WinSock2.h>
#pragma comment (lib,"WS2_32.lib")
附加一个 需要利用里面的一些定义
#include <Windows.h>
定义如下
我们进行严格的端口定义是需要定义为 WORD 即 unsigned short
端口的分配
为了方便端口的调配 ,我们讲端口的分配设置为动态 其中的InterInit函数为网络初始化函数
WORD port = 0;
int server_socker = InterInit(&port);
然后打印出信息
printf("httpd服务已经启动,正在监听 %d 端口..", port);
总的代码是这样的
#include <stdio.h>
#include <Windows.h>
#include <WinSock2.h>
#pragma comment (lib,"WS2_32.lib")
int InterInit(PWORD port);
int InterInit(PWORD port)//实现网络的初始化
{
return;
}
int main(void)
{
WORD port = 0;
int server_socker = InterInit(&port);
printf("httpd服务已经启动,正在监听 %d 端口..", port);
return 0;
}
协议的使用
官方文档
我们需要用到这个函数进行网路的初始化
WSAStartup函数
int WSAAPI WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
WSAStartup
- W:windows
- S:socket
- A:Asynchronous 异步
- 同步:阻塞、卡死状态
- 异步:多个工作同时进行
- Startup:启动
参数1:WORD wVersionRequested
调用者可以使用的Windows套接字规范的最高版本。 高位字节指定次要版本号; 低位字节指定主要版本号。
#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
#define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) | ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))
#define LOWORD(l) ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l) ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define LOBYTE(w) ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w) ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
参数2:LPWSADATA lpWSAData
指向WSADATA数据结构的指针,该数据结构将接收Windows套接字实现的详细信息。
typedef struct WSAData {
WORD wVersion;//我们要使用的版本
WORD wHighVersion;//系统能提供给我们的版本
#ifdef _WIN64
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
#else
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];//当前库的秒数信息,2.0是第2版的意思
unsigned short iMaxSockets;//返回可用的socket数量,2版本只有就没用了
unsigned short iMaxUdpDg;//UDP数据报信息大小,2版本只有就没用了
char FAR * lpVendorInfo;//供应商特定的信息,2版本只有就没用了
#endif
} WSADATA;
返回值 int
如果成功,则WSAStartup函数将返回0。 否则,它将返回下面列出的错误代码之一。
WSAStartup函数直接在该函数的返回值中返回扩展错误代码。 不需要调用WSAGetLastError函数,并且不应使用该调用。
Error code | Meaning |
---|---|
WSASYSNOTREADY | 基础网络子系统尚未准备好进行网络通信。 |
WSAVERNOTSUPPORTED | 此特定的Windows套接字实现未提供所请求的Windows套接字支持的版本。 |
WSAEINPROGRESS | Windows Sockets 1.1的阻止操作正在进行中。 |
WSAEPROCLIM | Windows套接字实现所支持的任务数已达到限制。 |
WSAEFAULT | lpWSAData 参数不是有效的指针 |
代码实现
int InterInit(PWORD port)//实现网络的初始化
{
WSADATA data;//初始化数据
//网络通信初始化
int ret = WSAStartup(MAKEWORD(1, 1), &data);
if (ret)
{
switch (ret)
{
case WSASYSNOTREADY:
printf("请检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("请重新启动");
break;
case WSAEPROCLIM:
printf("请尝试关闭不必要软件,以为当前网络运行提供充足的资源");
break;
}
return -1;
}
//返回值:套接字
return;
}
创建套接字
SOCKET WSAAPI socket(
int af, //地址族规范。 地址族的可能值在Winsock2.h头文件中定义。
int type,//新套接字的类型规范。
int protocol//要使用的协议。
);
int af表示套接字的类型(网络套接字 文件套接字)
#define PF_UNSPEC AF_UNSPEC
#define PF_UNIX AF_UNIX
#define PF_INET AF_INET
#define PF_IMPLINK AF_IMPLINK
#define PF_PUP AF_PUP
#define PF_CHAOS AF_CHAOS
#define PF_NS AF_NS
#define PF_IPX AF_IPX
#define PF_ISO AF_ISO
#define PF_OSI AF_OSI
#define PF_ECMA AF_ECMA
#define PF_DATAKIT AF_DATAKIT
#define PF_CCITT AF_CCITT
#define PF_SNA AF_SNA
#define PF_DECnet AF_DECnet
#define PF_DLI AF_DLI
#define PF_LAT AF_LAT
#define PF_HYLINK AF_HYLINK
#define PF_APPLETALK AF_APPLETALK
#define PF_VOICEVIEW AF_VOICEVIEW
#define PF_FIREFOX AF_FIREFOX
#define PF_UNKNOWN1 AF_UNKNOWN1
#define PF_BAN AF_BAN
#define PF_ATM AF_ATM
#define PF_INET6 AF_INET6
Af | Meaning |
---|---|
AF_UNSPEC 0 | 地址族未指定。 |
AF_INET 2 | Internet协议版本4(IPv4)地址族。 |
AF_IPX 6 | IPX / SPX地址族。 仅当安装了NWLink IPX / SPX NetBIOS兼容传输协议时,才支持此地址系列。WindowsVista和更高版本不支持此地址系列。 |
AF_APPLETALK 16 | AppleTalk地址族。 仅当安装了AppleTalk协议时才支持此地址系列。WindowsVista和更高版本不支持此地址系列。 |
AF_INET6 23 | Internet协议版本6(IPv6)地址族。 |
AF_IRDA 26 | 红外数据协会(IrDA)地址族。仅当计算机安装了红外端口和驱动程序时,才支持此地址族。 |
AF_NETBIOS 17 | NetBIOS地址族。 仅当安装了用于NetBIOS的Windows套接字提供程序时,才支持此地址系列。在32位版本的Windows上支持用于NetBIOS的Windows套接字提供程序。 默认情况下,此提供程序安装在32位版本的Windows上.NetBIOS的Windows套接字提供程序在64位版本的Windows(包括Windows 7,Windows Server 2008,Windows Vista,Windows Server 2003或Windows XP)上不受支持。 用于NetBIOS的Windows套接字提供程序仅支持将* type 参数设置为* SOCK_DGRAM **的套接字。用于NetBIOS的Windows套接字提供程序与[NetBIOS]不直接相关(https://docs.microsoft.com/en -us / previous-versions / windows / desktop / netbios / portal)编程界面。 Windows Vista,Windows Server 2008和更高版本不支持NetBIOS编程接口。 |
AF_BTH 32 | 蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。 |
int type数据流 数据报
#define SOCK_STREAM 1 /* stream socket */
#define SOCK_DGRAM 2 /* datagram socket */
#define SOCK_RAW 3 /* raw-protocol interface */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5
在Windows套接字1.1中,唯一可能的套接字类型是SOCK_DGRAM和SOCK_STREAM
int protocol 通信协议 TCP & UDP
#define IPPROTO_IP 0 /* dummy for IP */
#define IPPROTO_ICMP 1 /* control message protocol */
#define IPPROTO_IGMP 2 /* group management protocol */
#define IPPROTO_GGP 3 /* gateway^2 (deprecated) */
#define IPPROTO_TCP 6 /* tcp */
#define IPPROTO_PUP 12 /* pup */
#define IPPROTO_UDP 17 /* user datagram protocol */
#define IPPROTO_IDP 22 /* xns idp */
#define IPPROTO_ND 77 /* UNOFFICIAL net disk proto */
返回值
如果没有发生错误,socket将返回一个引用新socket的描述符。否则,将返回INVALID_SOCKET 的值,并且可以通过调用WSAGetLastError检索特定的错误代码
代码
int server_socket = socket(PF_INET,SOCK_STREAM, IPPROTO_TCP);
if (server_socker == -1) {
printf("socket function failed with error = %d\n", server_socker);
switch (server_socker)
{
case WSANOTINITIALISED:
printf(" A successful WSAStartup call must occur before using this function.");
break;
case WSAENETDOWN:
printf("The network subsystem or the associated service provider has failed.");
break;
case WSAEAFNOSUPPORT:
printf("The specified address family is not supported. For example, an application tried to create a socket for the AF_IRDA address family but an infrared adapter and device driver is not installed on the local computer.");
break;
case WSAEINPROGRESS:
printf("A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function.");
break;
case WSAEMFILE:
printf(" No more socket descriptors are available.");
break;
case WSAEINVAL:
printf("An invalid argument was supplied. This error is returned if the af parameter is set to AF_UNSPEC and the type and protocol parameter are unspecified.");
break;
case WSAEINVALIDPROVIDER:
printf(" The service provider returned a version other than 2.2.");
break;
case WSAEINVALIDPROCTABLE:
printf("The service provider returned an invalid or incomplete procedure table to the WSPStartup.");
break;
case WSAENOBUFS:
printf("No buffer space is available. The socket cannot be created.");
break;
case WSAEPROTONOSUPPORT:
printf("The specified protocol is not supported.");
break;
case WSAEPROTOTYPE:
printf("The specified protocol is the wrong type for this socket.");
break;
case WSAEPROVIDERFAILEDINIT:
printf("The service provider failed to initialize. This error is returned if a layered service provider (LSP) or namespace provider was improperly installed or the provider fails to operate correctly. ");
break;
case WSAESOCKTNOSUPPORT:
printf("The specified socket type is not supported in this address family.");
break;
default:
break;
}
}
优化
函数说明
perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到 标 准 设备 (stderr) 。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量error 的值来决定要输出的字符串。在库函数中有个error变量,每个error值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了error的值。perror函数只是将你输入的一些信息和现在的error所对应的错误一起输出。
void error_die(const char* str) {
perror(str);
}
设置端口套接字可复用特性(端口复用)
int PASCAL FAR setsockopt (
_In_ SOCKET s,
_In_ int level,
_In_ int optname,
_In_reads_bytes_opt_(optlen) const char FAR * optval,
_In_ int optlen);
//设置端口套接字可复用特性
int opt = 1;
ret = setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR,(const char*) & opt, sizeof(opt));
if (ret == -1) {
error_die("setsockopt");
}
绑定套接字和网络地址
//配置服务器的网络地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_port = htons(*port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP 服务器 INADDR_ANY 表示任意网络地址可接入
//绑定套接字
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
error_die("bind");
}
`
动态分配一个端口
//动态分配一个端口
int nameLen = sizeof(server_addr);
if (*port == 0) {
if (getsockname(server_socket, (struct sockaddr*)&server_addr, &nameLen) < 0)
{
error_die("getsockname");
}
*port = server_addr.sin_port;
}
创建监听队列
//创建监听队列
if (listen(server_socket, 5) < 0) {
error_die("listen");//监听队列报错
}
创建 客户端的访问
//客户端 访问
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
while (1) {
//
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sock == -1) {
error_die("accept"); //打印错误信息并结束
}
}
使用多线程进行用户的访问
DWORD dwThreadID = 0;
HANDLE handleFirst = CreateThread(NULL, 0, accept_request, (void*)client_sock, 0, &dwThreadID);
DWORD WINAPI accept_request(LPVOID arg) {
return 0;
}
使用CreateThread函数创建线程
线程是进程中的一个实体,是被系统独立调度和分派的基本单位。一个进程可以拥有多个线程,但是一个线程必须有一个进程。线程自己不拥有系统资源,只有运行所必须的一些数据结构,但它可以与同属于一个进程的其它线程共享进程所拥有的全部资源,同一个进程中的多个线程可以并发执行。
在C/C++中可以通过CreateThread函数在进程中创建线程,函数的具体格式如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadID
);
参数的含义如下:
lpThreadAttrivutes:指向SECURITY_ATTRIBUTES的指针,用于定义新线程的安全属性,一般设置成NULL;
dwStackSize:分配以字节数表示的线程堆栈的大小,默认值是0;
lpStartAddress:指向一个线程函数地址。每个线程都有自己的线程函数,线程函数是线程具体的执行代码;
lpParameter:传递给线程函数的参数;
dwCreationFlags:表示创建线程的运行状态,其中CREATE_SUSPEND表示挂起当前创建的线程,而0表示立即执行当前创建的进程;
lpThreadID:返回新创建的线程的ID编号;
如果函数调用成功,则返回新线程的句柄,调用WaitForSingleObject函数等待所创建线程的运行结束。函数的格式如下:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
参数的含义如下:
hHandle:指定对象或时间的句柄;
dwMilliseconds:等待时间,以毫秒为单位,当超过等待时间时,此函数返回。如果参数设置为0,则该函数立即返回;如果设置成INFINITE,则该函数直到有信号才返回。
一般情况下需要创建多个线程来提高程序的执行效率,但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对一个内存地址进行写入操作,由于CPU时间调度的问题,写入的数据会被多次覆盖,所以要使线程同步。
就是说,当有一个线程对文件进行操作时,其它线程只能等待。可以通过临界区对象实现线程同步。临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,Windows内部使用这个结构记录一些信息,确保同一时间只有一个线程访问改数据段中的数据。
使用临界区的步骤如下:
(1)初始化一个CRITICAL_SECTION结构;在使用临界区对象之前,需要定义全局CRITICAL_SECTION变量,在调用CreateThread函数前调用InitializeCriticalSection函数初始化临界区对象;
(2)申请进入一个临界区;在线程函数中要对保护的数据进行操作前,可以通过调用EnterCriticalSection函数申请进入临界区。由于同一时间内只能有一个线程进入临界区,所以在申请的时候如果有一个线程已经进入临界区,则该函数就会一直等到那个线程执行完临界区代码;
(3)离开临界区;当执行完临界区代码后,需要调用LeaveCriticalSection函数离开临界区;
(4)删除临界区;当不需要临界区时调用DeleteCriticalSection函数将临界区对象删除;
Http请求的流程
浏览器发起新的访问时,将向服务器端发送一个请求报文。例如,在浏览器地址输入 127.0.0.1:8000 回车后,服务器端收到的完整报文如下:
GET / HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: none\n
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n
请求报文由4四个部分组成:请求行、请求头部行、空行、请求数据。具体格式如下:
第一行报文详细说明
报文的第一行
报文的第一行是:GET / HTTP/1.1\nGET表示请求方法(另外有POST方法)
1.1表示http的版本
GET和HTTP之间的"/“表示请求的资源。浏览器发起请求后的,第一次发送的请求报文中,这个位置都是”/",/表示服务器端的资源目录,这里表示不指定特定的资源。
当服务器把网页文件(例如:index.html)发送给浏览器后,浏览器收到这个网页文件后,如果网页文件中含有图片,那么浏览器会自动再发起一个http请求报文,此时请求报文的第一行数据,就类似:
GET /images/head.png HTTP/1.1\n
响应报文的格式
服务器发送数据给浏览器时,发送的响应报文,由4个部分组成:
状态行、消息头部、空行和响应正文。格式如下:
常用的关键字有:
POST请求报文的格式
浏览器发送的POST报文的格式,和GET报文格式其实是一致的,只是多了最后一部分内容“请求数据”,实例如下
POST /color.cgi HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Content-Length: 9\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
Origin: http://127.0.0.1:8000\n
Content-Type: application/x-www-form-urlencoded\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: same-origin\n
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Referer: http://127.0.0.1:8000/\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n
color = red
最后一行“color=red”就是网页提交的数据
报文的分析
参考文献