目录
网络编程
网络编程,指的是直接或者间接通过网络协议与其他计算机进行通讯。
网络编程,主要点在二。其一,怎么精准定位位于网络上的一台或者多台主机;其二,找到主机后又改如何进行可靠高效的数据传输。针对上述2点,引出TCP/IP协议。在TCP/IP协议中IP层负责网络主机的定位,相当于数据传输的路由,由IP地址可以唯一的确定Internet中的一台主机。TCP层负责提供面向应用的可靠或者非可靠的数据传输机制。
现今较为流行的网络编程模型为CS(客户机/服务机)结构。通信双方一方作为服务器等候客户机提出请求并予以响应。客户机则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,以便后来的客户机也能得到服务。
通常一台主机上总是有很多个进程需要网络资源进行网络通讯。网络通讯的对象准确的讲不是主机,而是主机中运行的进程。此时,只有主机名+ip来标识这么多进程显然是不够的。端口号就是为了在一台主机上提供更多的网络资源而采取的一种手段,也是TCP层提供的一种机制。只有通过主机名、ip、端口号的组合才能唯一的确定网络通讯中的对象:进程。
套接字
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通讯机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
要通过网络进行通讯,至少需要一对套接字,一个运行于客户端的ClientSorket,一个运行于服务器的ServerSorket。根据链接启动的方式以及本地套接字要连接的目标,套接字之间的额连接可以分为三个步骤:服务器监听、客户端请求、连接确认。
- 服务器监听,是指服务器套接字并不定位具体的客户端套接字,而是出于等待连接的状态,实时监控网络状态。
- 客户端请求,是指由客户端的套接字提出连接请求,连接目标即为服务器端的套接字。所以,客户端的套接字必须首先描述它要连接的服务器套接字,指出服务器套接字的端口、ip,然后向该套接字提出连接请求。
- 连接确认,是指当服务器套接字监听到或者说收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字依旧处于监听状态,继续接收其他客户端套接字的请求。
Socket基础函数
- int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData);
WSAStartup,即WSA(Windows Sockets Asynchronous,Windows异步套接字)的启动命令。 WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。
参数说明:
⑴ wVersionRequested:一个WORD(双字节)型数值,在最高版本的Windows Sockets支持调用者使用,高阶字节指定小版本(修订本)号,低位字节指定主版本号。
⑵lpWSAData 指向WSADATA数据结构的指针,用来接收Windows Sockets [1] 实现的细节。
WindowsSockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
WSADATA w;
if (WSAStartup(MAKEWORD(1, 1), &w))
{
return FALSE;
}
- int socket(int domain, int type, int protocol);
参数说明:
(1)domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
(2)type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
(3)protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。2.WindowsSocket下protocol参数中不存在IPPROTO_STCP
返回值:
如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。
m_ClientSock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
- int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()或listen()调用前使用。当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
参数说明:
socket:是一个套接字描述符。
address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
address_len:确定address缓冲区的长度。
返回值:
如果函数执行成功,返回值为0,否则为SOCKET_ERROR。
sockaddr_in sa;
sa.sin_family = AF_INET;
DWORD dPort = g_dServicePort;
sa.sin_port = htons((unsigned short)dPort);
//InetPton(AF_INET, psAddr, &sa.sin_addr.S_un.S_addr);
sa.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (SOCKET_ERROR == bind(m_ServerSock, (LPSOCKADDR)&sa, sizeof(sa)) )
{
MessageBox(_T("bind失败!"), _T("提示"), MB_OK);
return 0;
}
- int listen(SOCKET sockfd, int backlog);
让一个套接字处于监听到来的连接请求的状态。listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。listen函数一般在调用bind之后-调用accept之前调用。
参数说明:
(1)sockfd 一个已绑定未被连接的套接字描述符
(2)backlog 连接请求队列(queue of pending connections)的最大长度
if (SOCKET_ERROR == listen(m_ServerSock, 5) )
{
MessageBox(_T("listen失败!"), _T("提示"), MB_OK);
UpdateUI(FALSE);
return 0;
}
- SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR *addr,int FAR *addrlen);
本函数从s的等待连接队列中抽取第一个连接,创建一个与s同类的新的套接口并返回句柄。如果队列中无等待连接,且套接口为非阻塞方式,则accept()阻塞调用进程直至新的连接出现。如果套接口为非阻塞方式且队列中等待连接,则accept()返回一错误代码。已接受连接的套接口不能用于接受新的连接,原套接口仍保持开放。
参数说明:
(1)s:套接口描述字,该套接口在listen()后监听连接。
(2)addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
(3)addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
返回值:
成功返回一个新的套接字描述符,失败返回-1。
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
while (true)
{
sClient = accept(m_ServerSock, (SOCKADDR*)&remoteAddr, &nAddrlen);
if (INVALID_SOCKET == sClient)
{
MessageBox(_T("accept失败!"), _T("提示"), MB_OK);
continue;
}
const char* chData = m_strSendData.c_str();
send(sClient, chData, strlen(chData) + sizeof(char), NULL);
}
- int select( int nfds, fd_set FAR* readfds, fd_set * writefds, fd_set * exceptfds, const struct timeval * timeout);
确定一个或多个套接口的状态,如需要则等待。
参数说明:
(1)nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
(2)readfds:(可选)指针,指向一组等待可读性检查的套接口。
(3)writefds:(可选)指针,指向一组等待可写性检查的套接口。
(4)exceptfds:(可选)指针,指向一组等待错误检查的套接口。
(5)timeout:select()最多等待时间,对阻塞操作则为NULL。
FD_SET fdset;
timeval tv;
FD_ZERO(&fdset);
//lint -save -e717
FD_SET(hSocket,&fdset);//lint -restore
nTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut;
tv.tv_sec = 0;
tv.tv_usec = nTimeOut;
int iRet = 0;
if (bRead)
{
iRet = select(0,&fdset,NULL,NULL,&tv);
}
else
{
iRet = select(0,NULL,&fdset,NULL,&tv);
}
if (iRet <= 0)
{
return FALSE;
}
- int PASCAL FAR send( SOCKET s, const char FAR* buf, int len, int flags);
向一个已经连接的socket发送数据,如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。
参数说明:
(1)s:一个用于标识已连接套接口的描述字。
(2)buf:包含待发送数据的缓冲区。
(3)len:缓冲区中数据的长度。
(4)flags:调用执行方式,一般为null。
send()适用于已连接的数据包或流式套接口发送数据。对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。IP包最大长度在WSAStartup()调用返回的WSAData的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。
请注意成功地完成send()调用并不意味着数据传送到达。
如果传送系统的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则send()将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select()调用来确定何时能够进一步发送数据。
参考accept处代码。
- int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);
从一个已经连接的socket接收数据。
参数说明:
(1)s:一个用于标识已连接套接口的描述字。
(2)buf:存放接收数据的缓冲区。
(3)len:缓冲区中数据的长度。
(4)flags:调用执行方式,一般为0。
注意:(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的);(3)recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
char chTemp[1024];
memset(chTemp, 0, sizeof(char) * 1024);
int iRead = recv(m_ClientSock, chTemp, 1024, 0);
if (iRead > 0)
{
……
}
- int PASCAL FAR closesocket( SOCKET s);
参数说明:
(1)s:一个套接口的描述字。
关闭一个套接口。更确切地说,它释放套接口描述字s,以后对s的访问均以WSAENOTSOCK错误返回。若本次为对套接口的最后一次访问,则相应的名字信息及数据队列都将被释放。
- int PASCAL FAR WSACleanup (void);
终止Winsock 2 DLL (Ws2_32.dll) 的使用。
参考链接:
1、https://blog.csdn.net/qq_36409711/article/details/78937137
2、https://blog.csdn.net/weixin_42755375/article/details/81783125
3、https://www.cnblogs.com/kefeiGame/p/7246942.html
4、https://www.cnblogs.com/weizhixiang/p/6298523.html
引用https://blog.csdn.net/qq_36409711/article/details/78937137的一个神图