? ? ? ? 目前关于 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。
?到此结束!本文于链接这篇博客中搬运了大量的知识!
|