IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> (初学者的福音)windows下实现socket通信(TCP/IP)代码详解——服务端篇 -> 正文阅读

[网络协议](初学者的福音)windows下实现socket通信(TCP/IP)代码详解——服务端篇

? ? ? ? 目前关于 socket 通信的教程并不少,但是存在一个现象:贴了代码的文章,对于代码的注释不够详细,导致读者频繁的去搜索某个函数的参数构成、使用方法等,十分地麻烦。我在进行学习的时候,在程序中的注释写得实在是太过于密密麻麻了,索性写个更详细的笔记,对用到的知识点进行一个综合的整理吧,省得一下子开十几个网页。。。

? ? ? ??本文的详解是基于windows环境下用c++实现socket编程这篇文章进行的。因此对于TCP/IP以及socket通信的基础知识就不在赘述了,本文着重于带你一行一行地对代码进行详细解释。具体的解释由网络搜集整理而成,感谢大佬们。

? ? ? ? 首先贴一个服务器端的完整代码:(注释写了一半,实在写不下去了,太多了)

/*****************************************************************************************************************************
*	1、加载套接字库,创建套接字(WSAStartup()/socket());
*	2、绑定套接字到一个IP地址和一个端口上(bind());
*	3、将套接字设置为监听模式等待连接请求;
*	4、请求到来之后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
*	5、用返回的套接字和客户端进行通信(send()/recv());
*	6、返回,等待另一个连接请求
*	7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
*****************************************************************************************************************************/
#include<iostream>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")

int main()
{
	//初始化WSA
	WORD sockVersion=MAKEWORD(2,2);
	WSADATA wsaData;//WSADATA结构体变量的地址值

	//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
	//成功时会返回0,失败时返回非零的错误代码值
	if(WSAStartup(sockVersion,&wsaData)!=0)
	{
		cout<<"WSAStartup() error!"<<endl;
		return 0;
	}

	//创建套接字
	SOCKET slisten=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(slisten==INVALID_SOCKET)
	{
		cout<<"socket error !"<<endl;
		return 0;
	}

	//绑定IP和端口
	sockaddr_in sin;//ipv4的指定方法是使用struct sockaddr_in类型的变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8888);//设置端口。htons将主机的unsigned short int转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址
	//bind函数把一个地址族中的特定地址赋给scket。
	if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("bind error !");
	}

	//开始监听
	if(listen(slisten,5)==SOCKET_ERROR)
	{
		cout<<"listen error !"<<endl;
		return -1;
	}

	//循环接收数据
	SOCKET sclient;
	sockaddr_in remoteAddr;//sockaddr_in常用于socket定义和赋值,sockaddr用于函数参数
	int nAddrlen=sizeof(remoteAddr);
	char revData[255];
	while(true)
	{
		cout<<"等待连接。。。"<<endl;
		sclient=accept(slisten,(sockaddr *)&remoteAddr,&nAddrlen);
		if(sclient==INVALID_SOCKET)
		{
			cout<<"accept error !"<<endl;
			continue;
		}
		cout<<"接收到一个连接:"<<inet_ntoa(remoteAddr.sin_addr)<<endl;
		//接收数据
		int ret=recv(sclient,revData,255,0);
		if(ret>0)
		{
			revData[ret]=0x00;
			cout<<revData<<endl;
		}
		//发送数据
		const char * sendData = "你好,TCP客户端!\n";
		send(sclient, sendData, strlen(sendData), 0);
		closesocket(sclient);
	}
	closesocket(slisten);
	WSACleanup();
	system("pause");
	//return 0;
}

接下来开始按行解释:

#include<WinSock2.h>

????????使用 socket 通信必须包含对应的头文件 <WinSock2.h> 。在添加头文件的时候能看到自动补全中还存在一个 <WinSock.h> 的头文件,那么这两者有啥区别呢?

????????<WinSock2.h> 设计的目的是替代 <WinSock.h>,而不是扩展它。在 <WinSock.h> 中定义的所有内容在 <WinSock2.h> 中也都定义了.

#pragma comment(lib,"ws2_32.lib")

????????#pragma ? comment(lib,"Ws2_32.lib")表示链接 Ws2_32.lib 这个库。 ?

????????这种方式和在工程设置_链接库里面添加 Ws2_32.lib 的效果一样,不过这种方法写的 ?
? 程序,别人在使用你的代码的时候就不用再设置工程了。

int main()
{
	//初始化WSA
	WORD sockVersion=MAKEWORD(2,2);
    WSADATA wsaData;//WSADATA结构体变量的地址值
}

?????????MAKEWORD?语法如下:

????????WORD MAKEWORD(

? ? ? ????????? BYTE below;? //指定一个低位的新值

? ? ????????? ? BYTE high;? //指定一个高位的新值

????????);

????????先将两个参数转换为二进制,然后将第一个参数放在低位,第二个参数放在高位,最后转换为十进制,赋给 sockVersion。

? ? ? ? 这一步是为了声明调用不同的WinSock版本。例如MAKEWORD(2,2)就是调用2.2版本,MAKEWORD(1,1) 就是调用1.1版。
????????不同版本是有区别的,例如1.1版只支持TCP/IP协议,而2.0版可以支持多协议。2.0版有良好的向后兼容性,任何使用1.1版的源代码、二进制文件、应用程序都可以不加修改地在2.0规范下使用。此外 WinSock 2.0 支持异步,1.1不支持异步。

? ? ? ? WSADATA 是一个结构体,用于存放 socket 的初始化信息。wsaData 用于存放结构体变量的地址值。

if(WSAStartup(sockVersion,&wsaData)!=0)
	{
		cout<<"WSAStartup() error!"<<endl;
		return 0;
	}

????????WSAStartup()的原型如下:?int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

? ? ? ? 如果WSA初始化成功,函数会返回0,失败时会返回非零的错误代码值。所以如果函数返回值不等于0,则打印错误信息,程序终止。

	//创建套接字
	SOCKET slisten=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(slisten==INVALID_SOCKET)
	{
		cout<<"socket error !"<<endl;
		return 0;
	}

????????socket 函数的原型为: int socket(int af, int type, int protocol);

????????socket 函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个 socket 。这个socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

????????af :即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。?

? ? ? ? type:指定 socket 类型。常用的 socket 类型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

??????? protocol:就是指定协议。常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它们分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议。

//绑定IP和端口
	sockaddr_in sin;//ipv4的指定方法是使用struct sockaddr_in类型的变量
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8888);//设置端口。htons将主机的unsigned short int转换为网络字节顺序
	sin.sin_addr.S_un.S_addr = INADDR_ANY;//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址
	

? ? ? ? 在上面的套接字类型选择的是 SOCK_STREAM。这种类型需要通信双方均具有地址,其中服务器端的地址需要明确指定,而 ipv4 的指定方法是使用 struct sockaddr_in 类型的变量。

? ? ? ? 因此我们先实例化一个 sockaddr_in类型的?sin ,用它来进行端口和 IP 地址的设置。

? ? ? ? sockaddr_in这个结构体的参数如下:这里写图片描述

? ? ? ? ?在程序中,我们用 sin 来分别进行三个参数的设置。具体内容已写在程序的注释中。

    //bind函数把一个地址族中的特定地址赋给scket。
	if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("bind error !");
	}

?? ? ? ? bind() 函数把一个地址族中的特定地址赋给 socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket。

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

? ? ? ??SOCKET: 即 socket 描述字,它是通过 socket() 函数创建了,唯一标识一个 socket。bind() 函数就是将给这个描述字绑定一个名字。?

? ? ? ??sockaddr: 一个 const struct sockaddr *指针,指向要绑定给 sockfd 的协议地址。

? ? ? ? namelen: 对应的是地址的长度。?通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的 ip 地址组合。这就是为什么通常服务器端在listen 之前会调用 bind(),而客户端就不会调用,而是在 connect() 时由系统随机生成一个。

    //开始监听
	if(listen(slisten,5)==SOCKET_ERROR)
	{
		cout<<"listen error !"<<endl;
		return -1;
	}

?????????作为一个服务器,在调用 socket() 、bind() 之后就会调用 listen() 来监听这个 socket,如果客户端这时调用 connect() 发出连接请求,服务器端就会接收到这个请求。函数原型如下:

int listen(int sockfd, int backlog);

? ? ? ? sockfd: 要监听的 socket 描述字。

? ? ? ? backlog: 相应 socket 可以排队的最大连接个数。

    //循环接收数据
	SOCKET sclient;
	sockaddr_in remoteAddr;//sockaddr_in常用于socket定义和赋值,sockaddr用于函数参数
	int nAddrlen=sizeof(remoteAddr);
	char revData[255];
	while(true)
	{
		cout<<"等待连接。。。"<<endl;
		sclient=accept(slisten,(sockaddr *)&remoteAddr,&nAddrlen);
		if(sclient==INVALID_SOCKET)
		{
			cout<<"accept error !"<<endl;
			continue;
		}
		cout<<"接收到一个连接:"<<inet_ntoa(remoteAddr.sin_addr)<<endl;
		//接收数据
		int ret=recv(sclient,revData,255,0);
		if(ret>0)
		{
			revData[ret]=0x00;
			cout<<revData<<endl;
		}
		//发送数据
		const char * sendData = "你好,TCP客户端!\n";
		send(sclient, sendData, strlen(sendData), 0);
		closesocket(sclient);
	}

?????????TCP服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket() 、connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。

? ? ? ? ? ?首先看看 accept() 函数的定义:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

? ? ? ? 不知道大家发现没有,accept 函数的第二个参数定义的是一个sockaddr 的结构体,但是在程序中传入的参数确是由结构体 sockeraddr_in 来定义的,这是为什么呢?

? ? ? ? 其实, struct sockadd r和 struct sockaddr_in 这两个结构体都是用来处理网络通信的地址。sockaddr 常用于 bind、connect、recvfrom、sendto等 函数的参数,指明地址信息,是一种通用的套接字地址。sockaddr_in 是 internet 环境下套接字的地址形式。所以在网络编程中我们会对 sockaddr_in 结构体进行操作,使用 sockaddr_in 来建立所需的信息,最后使用类型转化就可以了。一般先把 sockaddr_in 变量赋值后,强制类型转换后传入用 sockaddr 做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

? ? ? ? recv() 和 send() 函数分别用于接受和发送数据。如果是在 linux 下,则分别为 read() 和 send() 函数 。

    closesocket(slisten);

?????????closesocket() 函数关闭一个套接口。更确切地说,它释放套接口描述字 s,以后对 s 的访问均以 WSAENOTSOCK 错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。

WSACleanup();

?????????WSACleanup() 与开头的 WSAStartup() 函数是成对使用的,用于解除与 Socket 库的绑定并且释放 Socket 库所占用的系统资源。

????????在 Windows 下,Socket 是以 DLL 的形式实现的。在 DLL 内部维持着一个计数器,只有第一次调用 WSAStartup 才真正装载DLL,以后的 调用只是简单的增加计数器,而WSACleanup 函数的功能则刚好相反,每调用一次使计数器减1,当计数器减到0时,DLL 就从内存中被卸载!因此,你调用了多少次 WSAStartup ,就应相应的调用多少次的WSACleanup。

?到此结束!本文于链接这篇博客中搬运了大量的知识!

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:29:09  更:2021-08-14 14:31:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 19:44:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码