Windows网络编程(一)

Windows网络编程(一):创建链接

文章目录

    • Windows网络编程(一):创建链接
      • 一、使用前的准备
      • 二、使用中的初始化
      • 三、错误处理
      • 四、很重要的一个概念——IP寻址
      • 五、很重要的socket连接概念
      • 六、创建一个服务器的监听模式
        • 1. 服务器开启TCP的过程
        • 2. 客户端连接服务器的API函数
        • 3. UDP的数据传输连接过程
      • 七、关于socket编程中INADDR_ANY的理解
      • 八、开始传输数据
        • 两个传输层协议的数据传输
        • (一)TCP的数据传输
          • 1. 发送函数
          • 2. 接收函数
        • (二)UDP的数据传输
          • 1. 发送数据
          • 2. 接收数据
      • 九、错误处理

一、使用前的准备

  必须使用到的头文件是winsock2.hwinsock.h两个头文件中选择一个。

  需要注意的是,单纯的引入这个头文件是不能完成代码的编译的。因为winsock需要使用静态链接库WSOCK32.LIB进行静态链接。因此需要采用以下其中之一的方式将静态库进入,否则在编译的时候会出现以下的错误:

NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSAStartup,该符号在函数 main 中被引用
NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSACleanup,该符号在函数 main 中被引用
NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSAGetLastError,该符号在函数 main 中被引用
  • 第一种方法是在代码中进行连接:
#pragma comment(lib, "WSOCK32")
  • 第二种方法是在编译是进行连接:
clang++ .\NetDemo.cpp -o NetDemo.exe -L F:\uCard\VC6.0green\VC98\Lib\ -lWSOCK32

二、使用中的初始化

  包含头文件之后,需要的做的第二件事就是将winsock进行初始化,初始化的过程主要是使用这个函数加载合适的winsock DLL。然后才能进行使用,使用的初始化函数原型如下:

int WSAStartup(
	WORD wVersionRequested,
	LPWSADATA lpWSAData
);

  然后使用winsock进行socket编程,使用完winsock之后,最终需要将socket进行释放资源,使用的是以下的函数原型如下:

int WSACleanup();

本函数原型的使用实例是:

WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
WSACleanup();

三、错误处理

  在进行编程的时候需要对在网络中出现的问题进行错误检查和错误处理,在这里使用的函数原型如下所示:

int WSAGetLastError();

  这个函数接收的错误必须是在winsock被加载完成之后,才能进行错误的获取,否则不能获取错误。正是因为这个原因,这个函数调用需要在WSAStartup之后获取错误代码。

四、很重要的一个概念——IP寻址

  理解结构体——SOCKADDR_IN,这个结构体主要的是以下结构:

struct sockaddr_in
{
    short 			sin_family; // 协议栈
    u_short 		sin_port;	// 端口号
    struct in_addr 	sin_addr;	// 自己的IP地址
    char 			sin_zero[8];// 用于填充的字节
};

  接下来分别介绍以上结构体中的各字段:

  • s i n _ f a m i l y sin\_family sin_family:在IP协议栈中的值为AF_INET
  • s i n _ p o r t sin\_port sin_port:设置IP地址的端口号
  • s i n _ a d d r sin\_addr sin_addr:IP地址的一个结构体:很重要,以下就是针对这个IP地址进行处理。

  主要的问题就是将点分十进制转换成为一个长整型函数,主要的函数原型就是以下的函数:

unsigned long inet_addr(const char FAR * cp);

  这样可能存在一个问题,对于不同的主机厂商,他们设定的IP地址在本机中的序列可能有所不同,即我们所说的大端字节序和小端字节序的区别。然而在同一个网络上就需要将以上的IP地址进行统一,在这里我们统一使用大端字节序作为网络上IP的标准。

于是针对这一需求,就存在大量的函数用于解决这个问题,将主机字节序转换成为网络字节序。下列的四个API都能实现以上的需求:

u_long htonl(u_long hostlong);// 返回值就是最终的数据
int WSAHtonl(SOCKET s, u_long hostlong, u_long FAR * lpnetlong);// 返回值放置在lpnetlong中
u_short htons(u_short hostshort);
int WSAHtons(SOCKET s, u_short hostshort, u_short FAR * lpnetshort);

实现反方向转化的函数为:

u_long htonl(u_long netlong);// 返回值就是最终的数据
int WSAHtonl(SOCKET s, u_long netlong, u_long FAR * lphostlong);// 返回值放置在lpnetlong中
u_short htons(u_short netshort);
int WSAHtons(SOCKET s, u_short netshort, u_short FAR * lphostshort);

敬请期待

五、很重要的socket连接概念

  socket在网络编程中是一个很重要的概念,针对socket这个结构体有以下的结构:

SOCKET socket(
    int af;			// 协议的地址族
    int type;		// 套接字的类型
    int protocol;	// 使用的协议
);

  其实我们不难知道,socket是传输层的一个概念,因此以上的结构体如果我们采用TCP/IP协议族的话,那么以上结构体中各字段的含义如下:

  • a f af af:这个值就设置为和以上相同AF_INET
  • t y p e type type:这个值根据TCP和UDP的不同而设置为不同的关键字——对于TCP这个字段的值设置为 S O C K _ S T R E A M SOCK\_STREAM SOCK_STREAM,对于UDP这个字段的值设置为 S O C K _ D G R A M SOCK\_DGRAM SOCK_DGRAM ,其实也不难理解,因为对于TCP的传输就是使用的流式数据传输;而针对UDP采用的是数据包的形式进行数据传输
  • p r o t o c o l protocol protocol:这个就是协议了,根据使用的传输层协议设置这个字段,若采用的是TCP则本字段设置为 I P P R O T O _ T C P IPPROTO\_TCP IPPROTO_TCP,若使用的协议为UDP,那么本字段的值设置为 I P P R O T O _ U D P IPPROTO\_UDP IPPROTO_UDP

六、创建一个服务器的监听模式

  这个过程让我们来梳理一下,我们所说的服务器本质上就是一个进程,如果想要被客户端连接,那么服务器必须在一个已知名称(其实就是一个socket上,我们知道socket就是IP地址+端口号,时刻注意这个需要绑定的地址服务就是服务器自己的IP)上进行监听。那么这样一来整个服务器需要做的工作就比较明了了——

1. 服务器开启TCP的过程

  • 将WinSock所提供的套接字和已知的名称绑定起来。在这里使用的API b i n d ( ) bind() bind()
int bind(
	SOCKET 						s, // 等待客户连接的套接字,是客户的套接字
	const struct sockaddr FAR * name, // 就是一个普通的缓冲区。根据使用的协议必须实际地填充一个地址缓冲区,并在调用时,将其转成一个sockaddr
	int 						namelen// 由协议决定的要传递的地址结构的长度
);
SOCKET 			s;
SOCKETADDR_IN 	tcpaddr;
int 			port = 5150;

s = socket(AF_INET, sOCK_STREAM, IPPROTO_TCP);
tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));
  • 然后将绑定的已知的名称进行监听——这里使用的API l i s t e n ( ) listen() listen(),以等待客户端的连接,监听的函数原型是以下:
int listen(
	SOCKET 	s, 		// 这个是被绑定的套接字
	int 	backlog // 被阻塞的连接的最大队列长度 
);
  • 如果有客户端连接到这个服务器,那么服务器需要做的就是接受这个请求,并和他建立连接,这里使用的API a c c e p t ( ) accept() accept()
SOCKET accept(
	SOCKET 					s,
	struct sockaddr FAR * 	addr, // 用来存储请求客户端的IP地址
	int FAR * 				addrlen // 
);

2. 客户端连接服务器的API函数

客户端创建 s o c k e t socket socket连接服务器的主要过程是以下几步:

  1. 创建一个 s o c k e t socket socket

  2. 建立一个 S O C K A D D R SOCKADDR SOCKADDR的地址结构,这个地址是准备连接到服务器的IP地址,在 T C P / I P TCP/IP TCP/IP协议中,这个地址结构就是监听服务器的IP地址和端口号

  3. 使用 c o n n e c t connect connect初始化客户机与服务器的连接

int connect(
	SOCKET 						s,
	const struct sockaddr FAR * name,
	int 						namelen
);

3. UDP的数据传输连接过程

对于 U D P UDP UDP连接的 s e r v e r server server的主要工作的流程是以下的几步:

1)初始化 s o c k e t socket socket

2)将 s o c k e t socket socket绑定自己的 i p ip ip地址和端口

3) r e c v f r o m recvfrom recvfrom以获取客户端的 i p ip ip地址,然后进行通信

4) s e n d t o sendto sendto发送数据

对于 U D P UDP UDP连接的 c l i e n t client client的主要步骤是以下几步:

1)初始化 s o c k e t socket socket

2)使用 s e n d t o sendto sendto向服务器的 s o c k e t socket socket发送数据

存在的疑问就是:QQ用户即使在对方离线的情况下,是怎样能够收取对方在自己离线时发送给自己的信息

七、关于socket编程中INADDR_ANY的理解

  实际上这个INADDR_ANY转换成为点分十进制是0.0.0.0,这个地址让人感觉就比较迷茫。在网络地址中设置0.0.0.0这个地址的用意实际上是这样的——

  我们知道一台主机可能有多个IP地址,就比如我们之前学到的在进行环回测试时候的保留地址127.0.0.1,这个地址是不可能出现在任何公网情况下的,当我们的主机连接网络时,就会获取路由器为我们分配的一个IP地址,这样一看,我们的就存在两个IP。还有其他更多的情况是可能服务器有多个网卡什么的可能会有更多的地址,因此一台主机的IP地址可能存在多个

  即然存在以上的问题,(需求)那么我们当然希望访问主机的任意一个IP地址都能够得到服务器的响应,这其实就是存在的问题。如果我们按照原来的思路,将服务器的socket只绑定在一个IP上,那么结果就是——客户端只能通过这个唯一的IP访问服务器,剩下的IP就不能进行访问了。按照原来的绑定方式,那么只能将每个IP地址都绑定一个socket进行管理,这样就会相当繁琐,因此有以的方法——

  为了解决这个问题,于是设置将服务器的socket与0.0.0.0进行绑定,这个特殊的IP地址表示的是本机的所有IP地址,这样理解的话,也就是说我们使用一个socket就监听了好多地址,而且最后只需要管理一个socket。这就大大简化了最后的管理过程。

八、开始传输数据

两个传输层协议的数据传输

(一)TCP的数据传输

1. 发送函数

在已经建立连接的套接字上发送数据的主要API是 s e n d send send,这个函数的原型是以下的方式:

int send(
	SOCKET 				s,
	const char FAR * 	buf,//指向缓冲区的指针
	int 				len,//缓冲区的大小
	int 				flags
);
2. 接收函数

主要的套接字函数接收是主要是API是 r e c v recv recv,这个函数的原型是以下的方式:

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

(二)UDP的数据传输

1. 发送数据

在发送的过程中,使用的接口是 s e n d t o sendto sendto,这个函数的原型是以下的方式:

int sendto(
	SOCKET s,
	const char FAR * buf,
    int len, 
    int flags,
    const struct sockaddr * FAR * to,
    int len
);
2. 接收数据

在接收的过程当中,使用的接口是 r e c v f r o m recvfrom recvfrom,这个函数主要是以下的方式,通过这个函数可以获取发送方的IP地址和端口号.

int recvfrom(
	SOCKET s,
	char FAR * buf,
	int len, 
	int flags,
	struct sockaddr FAR * from,
	int FAR * fromlen
);

九、错误处理

w i n d o w s s o c k e t windows\quad socket windowssocket编程当中,也不是所有的都是一帆风顺的,因此对于错误的处理也应当足够引起我们的重视,一般来说,我们可以通过函数来获取 w i n s o c k winsock winsock的错误类型,从而定位错误。

int WSAGetLastError(void);

在连接过程当中发生错误之后,可以调用这个函数,就可以获取返回的错误的整数代码。这些值可能存在于 W I N S O C K 1. H WINSOCK1.H WINSOCK1.H W I N S O C K 2. H WINSOCK2.H WINSOCK2.H中。

你可能感兴趣的:(C++)