Windows 网络通信编程

前言

        Wiindows网络编程并不需要太多知识,只要你会C语言就能够轻松学会。在这里我将介绍如何在Windows系统中进行网络通信,并附上一个完整的简单的网络通信代码。

目录

Socket套接字

简单的网络通信流程

服务器代码编写

初始化

创建套接字

绑定端口

监听端口

接收客户端连接请求

接收数据

发送数据

关闭套接字

释放资源

客户端代码编写

连接服务器


Socket套接字

        想要学好网络编程,理解套接字是非常重要的。套接字是所有网络通信中必不可少的一部分。通俗地讲,套接字就是两个应用程序进行通信时各自连接的端点不要去管它为什么叫套接字,反正它就是两个应用程序通信的接口,暂时这么简单的理解就行了。

简单的网络通信流程

        想要进行网络通信就必然需要两个应用程序之间发送消息。

        这里我就用服务器\客户端架构来描述网络通信的流程。我也不扯太多底层的东西,只要通过这个流程能理解后面的代码是在干什么就行了。

        这是一个服务器和客户端的简单通信,可以从中看出客户端与服务器有许多相似之处。

        整个流程从服务器开始,服务器完成一系列准备操作后会停在accept函数这里,等待客户端的连接。客户端完成准备操作后会向服务器发送连接请求,成功建立连接后客户端和服务器接着进行后面的数据交流。服务器此时会停在第一个recv函数这里,等待接收客户端发送的请求数据。当客户端成功发送数据后也会停在recv函数这里并等待接收服务器发送的应答数据。数据接收成功后客户端将关闭套接字,这个操作会向服务器发送一个消息,告诉服务器我要结束连接。这时一个完整的服务器和客户端的通信流程就走完了。

        这只是一个简单的流程,在实际应用中客户端和服务器会发送和接收许多数据,并且服务器也会同时和非常多的客户端保持连接。仔细阅读上一段流程的描述,你会发现我说服务器或者客户端停在了accept函数和recv函数那里,这意味着这两个函数是阻塞函数。当应用程序执行该函数时会发生阻塞,看起来就是“该应用程序未响应”,直到接收到数据后才会继续执行后面的程序。因此,实际应用我们还需要利用多线程技术去解决这个麻烦的问题。这里我不展开介绍,只讲解最简单的网络通信流程。

服务器代码编写

        服务器是网络通信的核心枢纽,服务器代码的好坏决定了服务器的承受能力。

        套接字socket被定义在头文件WinSock2.h中,因此我们需要在源代码的开头包含它。同时我们还需要链接静态链接库ws2_32.lib

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

初始化

        所谓初始化,就是调用WSAStartup函数初始化Winsock DLL动态链接库。这里的WSAStartup就是为了向操作系统说明,我们要用哪个库文件,让该库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数了。

        为了初始化,我们需要创建WSADATA这种数据结构来保存WSAStartup函数返回的Windows Sockets数据。

	WSADATA dat;
	if (WSAStartup(MAKEWORD(2, 2), &dat) == 1)
	{
		printf("初始化失败!\n");
		WSACleanup();
		return;
	}
	else
	{
		printf("初始化成功!\n");
	}

        WSAStartup函数的第一个参数为我们需要的网络库的版本,第二个参数为我们创建的WSADATA变量的地址。这里的参数你基本可以不用管,照着葫芦画瓢就行了。

        WSAStartup函数初始化成功返回0,否则返回1。如果初始化失败,那么我们需要用WSACleanup函数来解除与Socket库的绑定和释放Socket库所占用的系统资源。

创建套接字

        做完初始化后,就开始创建套接字。在服务器的代码中有两种套接字,一种是用于通信的套接字,还有一种是用于监听的套接字,这里的套接字就是后者描述的套接字。

	SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (fd == -1)
	{
		printf("创建套接字失败!\n");
		WSACleanup();
		return;
	}
	else
	{
		printf("创建套接字成功!\n");
	}

        我们用SOCKET关键字创建套接字变量,然后将socket函数的返回值赋给这个变量。

        关于socket函数的第一个参数,我简单列表如下:

AF_INETIPV4
AF_INET6IPV6

        如果你使用的IP地址是IPV4,那么第一个参数为AF_INET,如果是IPV6则为AF_INET6。

        第二个参数为传输层协议的类型。这里我们填流式传输协议SOCK_STREAM。

        第三个参数为传输协议,我们填TCP协议IPPROTO_TCP。

        如果创建套接字失败,socket函数将返回-1。

绑定端口

        绑定端口就是将套接字和本地的IP端口进行绑定。

        绑定端口用bind函数,如果绑定失败返回-1,成功则返回0。它的第一个参数为套接字,第二个参数是一个用于存储端口信息的sockaddr类型的结构体指针,第三个参数为第二个参数所指向的结构体的字节大小。

        因为sockaddr类型的结构体不方便写入数据,因此我们用与它大小相同的sockaddr_in类型代替它写入数据后再进行强制类型转换。

        我们先创建一个sockaddr_in类型的结构体并为它赋值。成员sin_family为地址族协议,这里设置为AF_INET。成员sin_port为绑定的端口号,其范围为1~65535,这里要选择一个没有被占用的端口。成员sin_addr.S_un.S addr用于存储IP地址,这里设置为INADDR_ANY,这个参数就是inet_addr("0.0.0.0"),意思是本机的所有地址都可以使用。这里也可以调用inet_addr函数使用指定的IP地址。inet_addr函数的功能是将点分十进制的IP地址转换为网络字节序的长整型。

        这里讲一下字节序。字节序有大端和小端,网络字节序为大端,主机字节序为小端。所谓的大端和小端是指字节的顺序,在小端中低位字节存储到内存的低位,高位字节存储到内存的高位,即“低低高高”;而在大端中低位字节存储到内存的高位,高位字节存储到内存的低位,即“低高高低” 。

        因为字节序的原因,所以我们需要对大、小端的数据进行转换。htons函数的功能就是将主机字节序的端口号转换为网络字节序数据。

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(fd, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
{
	printf("绑定端口失败!\n");
	WSACleanup();
	return;
}
else
{
	printf("绑定端口成功!\n");
}

监听端口

        成功绑定端口后就可以开始监听这个端口了。监听端口就是为了检测新的客户端的连接请求。监听端口用listen函数,它的第一个参数为套接字,第二个参数是用于设置一次性可以检测多少个客户端的请求,最大的值为128。函数调用成功返回0,反之返回-1。

	if (listen(fd, 128) == -1)
	{
		printf("监听端口失败!\n");
		WSACleanup();
		return;
	}
	else
	{
		printf("监听端口成功!\n");
	}

接收客户端连接请求

        接收客户端连接请求用accept函数,它的第一个参数为监听的套接字,第二个参数为指向存储客户端地址族、端口号、IP地址等信息的结构体指针,这里的结构体与bind函数中的结构体类型相同。这里注意不能混淆,bind函数中的结构体是存储本地端口的信息,这里的结构体是存储客户端的信息。第三个参数为指向存储结构体字节大小的整型数据的指针。注意,这里的参数是指针类型,bind函数的第三个参数为整型。

        accept函数的返回值为用于通信的套接字,因此我们需要创建一个套接字接收accept函数的返回值。如果accept函数调用失败返回-1。

	struct sockaddr_in caddr;
	int addrlen = sizeof(caddr);
	SOCKET cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
	if(cfd == -1)
	{
		printf("连接客户端失败!\n");
		WSACleanup();
		return;
	}
	else
	{
		printf("连接客户端成功!\n");
	}

接收数据

        接收数据用recv函数,它的第一个参数为发送数据方的套接字,第二个参数为接收数据的缓冲区,简单来说就是一个指针。第三个参数为这个缓冲区的大小,单位是字节,最后一个参数一般置0。

int ret = recv(cfd, buff, 128, 0);

        recv函数的返回值有3种情况,列表如下:

ret > 0成功接收数据,返回值为数据的字节大小
ret = 0对方断开了连接
ret < 0接收数据失败

发送数据

        发送数据用send函数,它的参数列表和返回值与recv函数相同,这里就不在赘述了。

send(cfd, buff, 128, 0);

关闭套接字

        关闭套接字需要调用closesocket函数,它的参数就是需要关闭的套接字。

closesocket(cfd);

释放资源

        WSAStartup函数和WSACleanup函数总是成对出现,所以当前面初始化使用了WSAStartup函数后我们就需要在程序结束时调用WSACleanup函数来解除与Socket库的绑定和释放Socket库所占用的系统资源。

WSACleanup();

客户端代码编写

        客户端的代码仅有“连接服务器”这一步与服务器代码不同,这里就只介绍连接服务器的步骤。

连接服务器

        客户端连接服务器用connect函数,它与bind函数非常相似,第一个参数为用于通信的套接字,第二个参数为指向sockaddr类型的结构体指针,第三个参数为结构体的字节大小。

        在传入第二个参数前,我们要先准备好sockaddr_in类型的结构体。这里的端口号为服务器指定的端口,IP地址为服务器的IP地址。

	struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(9999);
	saddr.sin_addr.S_un.S_addr = inet_addr("xxx.xxx.xxx.xxx");
	if (connect(cfd, (struct sockaddr*)&saddr, sizeof(saddr)) == -1)
	{
		printf("连接服务器失败!\n");
		WSACleanup();
		return;					
	}
	else
	{
		printf("连接服务器成功!\n");
	}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

#include <bug>

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值