TCP / IP的工作
TCP / IP是Internet上使用的网络协议。它是协议,ESP32本身自带了TCP/IP协议,所以,我们只需了解并学会运用即可。
首先,有IP地址。这是一个32位值,应该是唯一的每个设备连接到互联网。一个32位的值可以被认为一个的 的四个不同的8位值(4-×8 = 32)。由于我们可以表示一个8位的数目为0到255之间的数值,我们通常代表与符号的IP地址:
<数字> <数> <数> <数>例如173.194.64.102。
这些IP address不常用作的应用程序输入。取而代之的是文本名称键入如“ google.com , 但不要被误导,这些名字是在TCP / IP的IL延髓水平。所有的工作都与32位的IP地址有一种映射。
需要一个名称(例如,“ google.com ”)来检索其对应的IP地址。 该技术,这就是所谓的“域名系统”或DNS。
当我们学习TCP / IP的,其实有三个不同的协议在这里。 第一个是IP(互联网协议)。这是下面的传输层数据报传递协议。再其上面的IP层是TCP(传输控制协议),其提供的在是无连接的IP协议的连接。最后是UDP(用户数据报协议),其在IP协议之上,并提供数据报在应用程序之间(无连接)传输。当我们说TCP / IP, 我们并不是说的刚才讲的在IP上运行TCP,但可看出作为一个核心协议,该协议是IP,TCP和UDP和其他相关应用水平协议,如DNS,HTTP,FTP,Telnet及更多。
轻量级IP协议栈 - LWIP
如果我们认为TCP / IP作为一种协议,那么我们就可以结束我们的理解联网成两个不同的层。
一个是负责硬件层:从一个地方到另一个地方获得的1个0的流 。对于常见的实现包括以太网,令牌环...这是由从设备物理线路特点。无线网络本身就是一个传输层。
一旦我们可以发送和接收数据,一个新的水平就在该数据物理网络上建立起来了,这便是TCP/IP通讯,它提供了硬件中的数据传输规则,但是TCP / IP是一个大的协议,它包含大量的部件。Espressif中为我们综合了LwIP轻便式通讯协议技术以方便开发,提供的LwIP包括下列服务:
• IP
• ICMP
• IGMP
• MLD
• ND
• UDP
• TCP
• sockets API
• DNS
TCP
TCP连接,通过该协议数据可以在两个方向上流动,在连接建立之前,它是被动监听传入的连接请求。连接的另一方负责启动连接,它主动请求连接形成。一旦连接形成,两边都可以发送和接受数据,为了“客户端”请求连接,它必须知道的地址信息,供服务器监听。 这个地址有两个不同的部分。第一部分是服务器是IP地址和第二部分是特定的“端口号”。我们无法看到一个ESP32如何设置自己为一个侦听传入的TCP / IP连接,这就要求我们开始了解的重要socket API
TCP连接过程
TCP连接过程需要三次交互才能完成,如下图所示:
首先客户端向服务器发售那个一个SYN报文段指明客户端打算连接的服务器端口,以及出生序号,服务器发回包含服务器初始序号的SYN报文段作为应答,接着,客户端对服务器的SYN报文段进行确认。这三次报文段完成连接的过程,称为三次握手。
TCP关闭过程
终止一个连接需要四次握手,如下图所示
产生四次握手的原因是由于TCP的半关闭造成的,既然一个TCP连接是全双工的,那么每个方向必须单独的进行关闭,原则就是当一方完成它的数据发送过程后就能发送一个FIN来终止这个方向连接,当一端收到一个FIN,他必须通知应用层另一端已经终止了哪个方向的数据传送。发送FIN通常是应用层进行关闭的结果,收到一个FIN只意味着这一方向没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。
TCP/IP Sockets(对于详细的socker API可看我的这篇随笔: SOCKET API)
TCP/IP socker API是一个编程接口,它是网络编程中最重要的API,其根据不同模式的编程风格不同:
对于TCP服务器是通过建立:
1.创建TCP套接字
2.关联本地端口与插座
3.设置 套接字监听模式
4.接受来自客户端的新连接
5.接收和发送数据
6.关闭客户机/服务器连接
7.返回到步骤4
对于TCP客户端建立:
1.创建TCP套接字
2.连接到TCP服务器
3.发送数据/接收数据
4.关闭连接
其编程模型如下所示:
socket API头定义中可以找到 <LWIP / sockets.h> 。对于客户端和服务器,创建套接字的任务是一样的,调用
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
返回 的sock 是用来指向套接字的整数句柄。
当我们创建了一个服务器端的socket,我们希望它监听传入连接要求。要做到这一点,我们需要告诉socket 哪个TCP/IP端口他需要监听(注意,我们并不提供端口类型是int还是short),我们通过调用htons()函数提供类型,它的功能是将数据转换为我们的网络字节顺序,在互联网上多字节的二进制数据实际是“大端”的格式,如9876(Decima的 升),那么它以二进制表示为00100110 10010100或0x26D4的十六进制。对于网络字节传输顺序,我们首先传送10010100(0xD4),再传输00100110(0×26),而ESP32是一个 小端机体系结构,这意味着我们必须改造2字节和4张字节数为网络字节顺序(big endian)的。
在给定的装置中,在一个时间只有一个应用程序可以使用给定的本地端口,如果我们想端口关联与应用,我们可以调用bind()函数来完成,下面给出一个实例:
struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(portNumber); bind(sock, (struct sockaddr *)&serverAddress, sizeof(serverAddress));
现在socket已经和相关的接口连接起来了,我们下一步就要开始调用listen()函数来监听输入的数据,listen() 接口函数看到如下:
listen(sock, backlog)
这里的backlog是,当我们监听的接口上esp32发生多次请求时,由于无法立即处理便会产生backlog(积压),这里是对积压值进行设定,当发送请求的数目大于谁的那个的backlog数时,ESP32便不会将这个请求放入积压队列中,而是立刻拒绝这个请求,这样不仅防止了是空的资源消耗再服务器上,也可以作为指示给调用者。从服务器的角度来看,我们还需要做一些 工作,当服务器正在处理一个客户端的请求时,此时另外一个客户端也发清了对端口的请求,此时,accept() API即可调用解决这个问题,当accept()被调用时,下面两中情况中的一种可能发生:如果没有客户端连接等待者,我们将阻塞等待直到客户端连接到来。另一种情况是,如果已经有一个客户端在那等待链接了,我们将立刻处理连接。这辆中情况的区别在于我们是否需要等待连接到来。
API调用示例如下:
struct sockaddr_in clientAddress; socklen_t clientAddressLength = sizeof(clientAddress); int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength);
需要关注的是,从accpt()返回的是一个新的socket(整数句柄)。
和所有的TCP连接是相似的,连接是对称和双向的,这意味着,不再具有客户端服务器的概念,双方都可以发送和接收,不同的是,我们没有必要调用bind()/listen()/accept()
struct sockaddr_in clientAddress; socklen_t clientAddressLength = sizeof(clientAddress); int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength);
SOCKET系列函数
1、socket函数:函数功能是打开网络通信接口,为了执行I/O操作,第一件要做的事情是调用socket函数,socket函数的原型如下:
socket(int falmily, int type, int protocol);
这里family指明协议簇,取值如表所示:
通常情况下我们都是用AF_INET,但是IPv6大规模的普及,AF_INET6取值也会广泛用到,在某些程序中,可能还会看到PF_INET等以PF为前缀的宏,最早的时候定义AF_表示地址簇,PF表示协议簇,但是现在PF已经很少使用了。
type指明了套接字的类型,取值如下表:
通常使用SOCK_STREAM 和SOCK_DRGRAM取值,当使用TCP或者SCTP时,就取SOCK_STREAM,当使用UDP时就用SOCK_DGRAM。
protocol参数指明协议类型,取值如下表所示:
socket函数在成功时返回一个小的非负整数,他和文件描述符类似,我们称为套接字描述符。
2、connect函数:TCP客户端用来和TCP服务器建立连接的,原型如下:
int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen);
sockfd参数是由socket函数返回的套接字描述符。
aervaddr参数是需要连接的远端的服务器的地址信息。
addrlen参数则是servaddr的字节大小。
connect()连接成功返回0,出错返回-1,返回-1后可以获取错误码得到具体的失败的原因。当TCP调用connect函数后,就会触发一个三次握手过程,这里强调TCP的元婴是因为使用UDP的时候也可以调用connect函数。
3、bind函数:把一个本地协议地址赋予一个套接字,更简单点来说,就是将本地IP地址和端口与套接字绑定在一起。
bind函数原型如下:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd参数是由socket函数返回的套接字描述符。
myaddr参数是本地的需要绑定的地址信息。
addrlen参数则是myaddr的字节大小。
bind()成功返回0,失败返回-1。
bind函数会应为myaddr参数设置不同情况,表现出不同的行为:
1)TCP服务器端。服务器端不指定端口的情况非常少见,应为客户端需要知道服务器的端口号,如果不指定的话,内核会指定一个临时端口,通过一些其他措施来通知客户端自己的端口号,如果不指定的话,内核会指定一个临时端口,在RPC(Remote Procedure Call,远过程调用)服务器中就会不指定端口,通过一些其他措施来通知客户端自己的端口,服务器端不指定地址的话,内核就会把客户端发送给SYN时携带的目的IP地址作为服务器的源地址。如果制定IP地址的话,那么服务器就只接收这个IP地址的数据。
2)TCP客户端。客户端一般不需要调用bind函数,在这种情况下,内核会根据外出接口绑定一个IP地址,并临时指定一个端口。如果调用bind函数的话,那么就会使用制定的IP或者端口。
3)UDP服务器端:服务器端不指定IP地址,套接口会接收到达它绑定端口的任何UDP数据报。并以数据报的外出接口的主IP地址为源IP地址,以接收到的源IP地址作为它的目的IP地址发回应答。当指定定本机IP地址,这就限制了套接口只接收到达它绑定端口并且目的地址为此IP地址的UDP数据报。并以绑定的IP地址作为源IP地址,以接收的源IP地址作为它的目的IP地址发回应答。
4)UDP客户端。和TCP客户端的行为类似,若UDP客户端未绑定IP地址,当它调用sendto时内核会根据外出接口给它绑定一个IP地址和一个临时端口号,若UDP客户端绑定了IP地址,他就为发出的数据报指定了一个源IP地址,并且UDP服务器在接到这个数据报后会以这个IP地址作为回应数据报的目的IP地址。
对于不指定地址的情况,我们称之为通配地址,使用常量INADDR_ANY表示,这个值通常也是0,在不指定端口的情况,就是端口为0,下表为这几种组合的情况
bind地址组合
4、listen函数:仅在TCP服务器调用,监听客户发起的connect,如果监听到客户的connect,则和额客户进行三次握手,listen函数的原型如下:
int listen(int sockfd, int backlog);
sockfd参数是由socket函数返回的套接字描述符。
backlog参数表示最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。
listen()成功返回0,失败返回-1.
5、accept函数:当完成三次握手后,接受这个连接,从未完成连接队列转移到已完成连接队列。accept函数原型如下:
int accept(int socket, struct sockaddr *cliaddrm, socklen_t *addrlen);
、 sockfd参数是由socket函数返回的套接字。
cliaddr参数是一个传出参数,accept()返回时传出客户端的地址和端口号。
addrlen参数是一个传入传出参数,传入的是调用者提供的缓冲去cliaddr的长度,以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度,如果给cliaddr参数传NULL,表示不关心客户端的地址。
accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过connfd通信,最后关闭connfd断开连接,而不关闭listenfd。accept()成功返回一个文件描述符,出错返回-1。
6、recv和send函数:recv函数和send函数分别是接收和发送数据的函数,在有些地方通常也使用read和write来代替这两个函数。recv和send函数的原型如下;
SSIZE_T recv(int sockfd, void *buff, size_t nbytes, int flags); SSIZE_T send(int sockfd, const void *buff, size_t nbytes, int flags);
sockfd参数对于recv来说就是接收端的描述符,对于send来说就是发送端的描述符。如果是服务器端,就是accept返回的描述符,如果是客户端时,就是socket返回的描述符。
buff参数是数据缓冲去,对recv来说就是接收数据的缓冲区,对于send来说就是发送数据的缓冲区。
nbytes参数是缓冲去的字节大小。
flags参数是一些手法的特殊标记,值一般为0或下表的取值。
收发特殊标记
recv和send函数如果成功都会返回接收或者发送的数据字节数,否则返回-1。
1) recv先等待s的发送缓冲区的数据被协议传送完毕,如果协议在传送sock的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR
2) 如果套接字sockfd的发送缓冲区中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd的接收缓冲区中没有数据或者协议正在接收数据,那么recv就一起等待,直到把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据copy到buff中(注意协议接收到的数据可能大于buff的长度,所以在这种情况下要调用几次recv函数才能把sockfd的接收缓冲区中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)
3) recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR。如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
4) 在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用 recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR;
2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes
3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完
4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的,send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。
5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。
6) send函数把buff中的数据成功copy到sockfd的改善缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send的socket函数在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传递完毕才能继续,如果在等待时出现网络错误那么该socket函数就返回SOCKET_ERROR)
7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。
6、close函数:用来关闭socket,并且终止TCP连接。其函数原型如下:
int close(int sockfd);
参数sockfd就是需要关闭的套接字的描述符。
close函数默认行为是吧套接字标记为已关闭,然后立即返回调用进程。此时,调用进程中将不能再使用该描述符。值得注意的是,函数是立即返回,其中的含义就是TCP连接并不是立即被终止,也就是说,尽管close函数已经返回,但是TCP协议还在工作,还要尝试将在缓冲区中未发送的数据发送到对端,然后在进行四次交互的关闭流程。对于这种行为可以使用套接字选项SO_LINGER来改变。
7、shutdown函数:shutdown也是关闭socket,并且终止TCP连接,通常情况况下都会使用close函数进行关闭,但某些情况下也可以使用shutdown函数。shutdown函数的原型如下:
int shutdown(int sockfd, int howto);
参数sockfd就是需要关闭的套接字的描述符。
参数howto表示关闭选项。选项值如下:
SHUT_RD,取值为0,表示关闭连接的杜这半部;SHUT_WR,取值为1,表示关闭连接的写这半部;SHUT_RDWR,取值为2,表示关闭连接的读这半部和写这半部;当参数取2时的效果和连续调用两次shutdown函数分别取0和1的效果相同。这个涉及TCP的半关闭概念,详情可以查看查理。史蒂芬文斯的《TCP-IP详解卷 I:协议》。
SOCKET中的地址转换
sockaddr_in,sockaddr,in_addr在socket中都有应用,这里来对它们进行区分
sockaddr和sockaddr_in在字节长度上都为16个BYTE,可以进行转换
struct sockaddr { unsigned short sa_family; //2 char sa_data[14]; //14 };
上面是通用的socket地址,具体到Internet socket,用下面的结构,二者可以进行类型转换
struct sockaddr_in { short int sin_family; //2 unsigned short int sin_port; //2 struct in_addr sin_addr; ‘//4 unsigned char sin_zero[8]; //8 }; struct in_addr就是32位IP地址。 struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; #define s_addr S_un.S_addr };
或者;
struct in_addr { in_addr_t s_addr; };
结构体in_addr 用来表示一个32位的IPv4地址
inet_addr()是将一个点分制的IP地址(如192.168.0.1)转换为上述结构中需要的32位二进制方式的IP地址(0xC0A80001)。//server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
通常的做法是:填值的时候使用sockaddr_in结构,而作为函数(如bin, accept, connect等)的参数传入的时候转换成sockaddr结构就行了,毕竟都是16个字符长。
通常的用法是:
int sockfd; struct sockaddr_in my_addr; //赋值时用这个结构 sockfd = socket(AF_INET, SOCK_STREAM, 0); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(MYPORT); my_addr.sin_addr.s_addr = inet_addr("192.168.0.1"); bzero(&(my_addr.sin_zero), 8); bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));//用(struct sockaddr *)转换即满足要求 //int accept(int s,struct sockaddr * addr,int * addrlen);//这三个函数的第二个参数结构都为struct sockaddr,所以一般做法都如上所示。 //int bind(int sockfd,struct sockaddr * my_addr,int addrlen); //int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
struct sockaddr 是一个通用地址结构,这是为了统一地址结构的表示方法,统一接口函数,使不同的地址结构可以被bind() , connect() 等函数调用;struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,他非常的常用,以至于我们都开始讨论它与 struct sockaddr通用地址结构的区别。另外还有struct sockaddr_un 地址结构,我们可以认为 struct sockaddr_in 和 struct sockaddr_un 是 struct sockaddr 的子集。
到这里,SOCKET编程的基本函数就介绍完成了,下篇文章: