目录
引流:Socket编程模型、网络编程模型、IO模型
使用模型
在编程中最简单最初使用的基本都是阻塞模型,比如fread,fwrite。它仅仅针对当前的线程,且对应的IO操作一定是在函数调用时开始,在完成或者失败时返回。代码逻辑简单、容易理解适合一些简单的场景,例如一些简单的测试用例。
代码示例
tcp 简单服务端代码
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<windows.h>
#include<WinSock2.h>
#include<stdio.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
//启动windows socket 2.x 环境
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
//--用socket API建立简易TCP服务端
// 1、建立一个socket 套接字
//AF_INET ipv4网络 , SOCK_STREAM 对象为数据流,
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2、绑定用于接受客户端连接的网络端口
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1");
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
printf("ERROR,绑定用于接受客户端连接的网络端口错误... \n");
}
else {
printf("绑定端口成功... \n");
}
// 3、listen监听网络端口
if (SOCKET_ERROR == listen(_sock, 5))
{
printf("ERROR,listen监听网络端口错误... \n");
}
else {
printf("listen监听网络端口成功... \n");
}
// 4、accept 等待接受客户端连接
sockaddr_in clientAddr = {};
int nAddrLen = sizeof(clientAddr);
SOCKET _cSock = INVALID_SOCKET;
while (true)
{
_cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == _cSock)
{
printf("错误,接收到无效客户端SOCKET... \n");
}
printf("新客户端加入: IP= %s \n", inet_ntoa(clientAddr.sin_addr));
// 5、send 向客户端发送一条数据
char msgBuf[] = "Hello,I'm Server.";
send(_cSock, msgBuf, strlen(msgBuf) + 1, 0);
}
//6、关闭套接字 closesocket
closesocket(_sock);
//清除windows socket环境
WSACleanup();
return 0;
}
关键API
socket()
该API创建了一个套接字描述符,该函数调用完只是得到一个描述符,并未与任何硬件或结构建立关联。调用后系统创建一个套接字,存在于一个名字空间(地址族)中,此时匿名。
SOCKET socket(int af,int type,int protocol);
af
一个地址描述。仅支持AF_INET格式,也就是说ARPA Internet地址格式。
type
名称 | 含义 |
SOCK_STREAM | 提供面向连接的稳定数据传输,即TCP协议。(TCP) |
SOCK_DGRAM | 使用不连续不可靠的数据包连接。(UDP) |
SOCK_RAW | 提供原始网络协议存取。(udp的以太网数据帧) |
SOCK_RDM | 提供可靠的数据包连接。(可靠UDP即保证交付数据报但不保证顺序) |
SOCK_SEQPACKET | 提供连续可靠的数据包连接。 |
protocol
名称 | 含义 |
IPPROTO_ICMP | ICMP报文 |
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
bind()
bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。在connect()或listen()调用前使用,connect前可省略,会默认绑定一个很大的端口。
int bind(SOCKET s,struct sockaddr * addr,int namelen);
s
使用socket()创建出的套接字。
addr
指向sockaddr结构体类型的指针,该结构使用 SOCKADDR_IN 等效替换。
// 绑定网络通信的地址
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET;
sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); //如INADDR_ANY -任何地址都可以
sockAddress.sin_port = htons(6666);//端口
namelen
addr结构的长度,可以用sizeof操作符获取。
注:
当调用此函数成功后,意味着套接字与本机一个准确的网络通信地址建立了绑关系。也可从套接字取出绑定的信息,可使用getsockname()来获知所分配的地址。
listen()
listen()是客户端和服务端第一次开始出现不同处理流程的地方,listen()为服务端必定调用的函数。主要作用就是将上述得到的套接字描述子变成一个被动监听的套接字, 用来被动等待客户端的连接。
创建套接口并为申请进入的连接建立一个后备日志队列(未完成连接队列)和已完成连接队列。
该函数仅仅是通知内核此套接字应如此处理,但进行连接等操作并非该函数完成。是由内核网络相关的服务模块完成,即可以认为该函数仅仅是一个配置设置函数。内核是独立的线程在完成套接字的连接等操作。
因此未完成连接队列和已完成连接队列都是由于内核维护:
未完成连接队列
【存储着尚未建立连接的套接字】 还未完成tcp三次握手的套接字 |
已完成连接队列
【存储着已经完成连接的套接字】 成功进行tcp三次握手的套接字 |
int listen(SOCKET s,int backlog);
s
前文用到的套接字。
backlog
backlog指代上述的内核维护的未完成连接队列和已完成连接队列,这两个队列大小之和。
收到客户端的连接请求之后, 内核创建一个套接字存储在未完成连接队列中, 来进行三次握手建立连接。
连接建立完成以后, 这个套接字就加到已完成连接队列的队尾, 服务器从已完成连接队列中取走一个, 又空出一个位置, 然后已经完成连接的套接字由补充进来, 就这样实现动态平衡。
因此如果在 listen 之后不进行 accept , connect 也是会成功返回的, 因为此时连接就已经建立好了。
参考文章
listen(), connect(), accept() 三者的关系
accept()
accept() 函数的作用就是在已完成连接队列中取出一个已经建立好的连接.
如果这个队列中已经没有已完成连接的套接字, 那么 accept() 就会一直阻塞, 直到取得一个已经建立连接的套接字
SOCKET accept(SOCKET s,sockaddr * addr,int FAR * addrlen);
s
前文用到的套接字。
addr
指针,指向一个sockaddr结构缓冲区,用来返回连接者的地址信息。
addrlen
指针,配合前一个参数,告知调用者获得的数据长度。
返回值
指代远端连接者的套接字,即服务端要向连接着的客户端发送或接收数据则使用这个套接字指代。
connect()
connect()用于建立与指定socket的连接。
通常客户端通过 connect() 函数来向服务端主动发起连接, 但是建立连接也不是这个函数完成的, 而是由内核完成的, 这个函数仅仅是通知内核通过三次握手建立连接, 然后将结果返回给这个函数.
这个函数默认会一直阻塞, 直到内核连接建立成功或者超时失败才返回(但一般这个过程很快)
int connect(SOCKET s,sockaddr* name,int namelen);
sockfd
标识一个套接字。
serv_addr
套接字s想要连接的主机地址和端口号。
addrlen
addr结构的长度,可以用sizeof操作符获取。