网络通信作为互联网的技术支持,已被广泛应用在软件开发中,无论是Web,服务端,客户端还是桌面应用,都是必须掌握的一门技术。
什么是网络编程?
在软件开发层面实现远程数据交换的编程技术。复制代码
网络编程的主要场景
- 基于Http/Https的web,移动端的开发;
- 基于TCP/UDP的IM,桌面应用的开发;
- 自定义协议的开发。
要熟悉网络编程,首先需要学习网络协议的相关知识。
网络协议
什么网络协议呢?网络协议是为网络中进行数据交换定义的规则,以实现按此规范进行传输数据,就可在整个互联网中进行数据交换的目标。所以说网络协议是网络通信的基础。
网络协议中其中最著名就是TCP/IP协议族。TCP/IP协议族通常被认为是一个四层的协议系统。从上到下依次为:
- 应用层:包括所有和应用程序协同工作,利用基础网络交换应用程序专用的数据协议,如Telnet, FTP, Http等;
- 运输层:负责为应用层提供端到端的通信支持,包含TCP和UDP协议;
- 网络层:负责数据包在网络中的传输,包含IP, ICMP和IGMP协议;
- 链路层:用以处理与物理设备的交互细节,对应于操作系统中的设备驱动程序;
从TCP/IP协议族各层次的职责来看,网路数据的传递是从上到下依次传递。以Http协议为为例,具体的数据传输过程如图所示:
当然,这只是数据的流向,实际上网络数据在传输过程中需要封装与分解,具体的过程如图所示(图片来源于百度图片):
简单来说,数据从本机上传到网络之前,会在TCP/IP协议族的每一层添加协议首部,到达目标主机后,目标主机再进行分解,从而获取需要的数据。
在众多的网络协议中,使用最广泛的应该就是TCP协议(传输控制协议)了,它是一种面向连接,可靠的,基于字节流的传输层协议。其运行过程分为三个阶段:建立连接、交换数据、断开连接。
TCP连接的建立与终止
在使用TCP协议交换数据前,需先建立一条连接,在不需要发送数据的时候,需要断开连接,以释放资源。
建立连接
TCP协议建立连接时需要三次握手,其过程可使用以下场景来描述:
面试官:说一下TCP建立连接时三次握手的过程。小明: 三次握手?面试官: 嗯。小明:握手完成了。面试官:what?复制代码
没错,从小明开始问三次握手到说握手完成就是三次握手的过程,具体的过程如下:
- 客户端发向服务端发送一个SYN段,以告知客户端要连接服务端的指定端口;
- 服务端收到客户端的报文后,返回一个对客户端报文进行确认的ack段(SYN+1)和一个表示服务端报文的SYN段;
- 客户端收到服务端发送的报文后,对服务端发送一个ack(服务端的SYN+1),至此,客户端和服务端就建立了一条连接。
为什么需要3次握手而不是2次呢?
因为客户端收到服务端的应答后,知道连接已建立成功,但是服务端并不知道自己发送的确认报文客户端是否收到,所以需要客户端对服务端的报文进行确认。复制代码
服务端一定能收到客户端发送的确认报文么?
不一定,如果收不到,那么连接就不会建立,所以,3次握手只是理论上确保建立连接的次数。那能否通过4次握手呢?不行,再握下去就是鸡生蛋,蛋生鸡的问题了。复制代码
断开连接
TCP断开连接时需要四次握手,为什么需要4次呢?这是由于TCP半关闭的性质造成的。所谓半关闭,就是可以发送数据,却不能接收数据或只能接收数据,不能发送数据。
四次握手的过程:(由于主动断开连接可发送在客户端,也可发送在服务端,所以下面以A,B来区分两端)
- A端向B端发送一个FIN, 告知A端即将断开连接;
- B端收到A端发送的FIN后发送一个ack对其进行确认;
- 随后B端向再A端发送一个FIN,以告知B端也即将断开连接;
- A端时候到B端的FIN报文后,对其发送一个ack以示确认。
B端在对A端进行确认的时候为什么不同时发一个FIN呢? 可以同时发。分开发是为了考虑B端在对A端确认后,可能还会给A继续发送数据的情况。
TCP的状态变迁过程
下面以一张图来描述TCP的状态变迁过程(图片来自百度图片)
图片中的所有状态对应于TCP的建立和连接过程,下面简单介绍一下这几种状态:
LISTEN: 服务端状态,表示服务端正在等待客户端的连接请求,处于监听状态;SYN收到:服务端状态,服务端已收到客户端的连接请求,并对客户端的请求发送了ACK确认(第二次握手完成);SYN_SENT: 客户端状态,客户端发送SYN或数据后的状态(第一次握手完成);ESTABLISHED: 客户端对服务端的SYN进行确认后处于ESTABLISHED,服务端收到客户端发送的ACK后也会处于 ESTABLISHED状态(三次握手完成后的状态);FIN_WAIT_1: 主动关闭的一端的状态,发送FIN后的状态(断开连接时的第一次握手完成);FIN_WAIT_2: 主动关闭的一端的状态,收到另一端的ACK确认后的状态(断开连接时的第二次握手完成);CLOSING:主动关闭的一端的状态,收到另一端的FIN,并对其进行确认后的状态(客户端和服务端同时关闭的情况);TIME_WAIT: 主动关闭的一端的状态,,收到另一端的FIN或(FIN和ACK)后,对其进行确认后的状态 (断开连接时最后一次握手完成);CLOSE_WAIT: 被动关闭的一端的状态,收到另一端发送的FIN并对其进行确认后的状态(断开连接时第二次握手完成);LAST_ACK: 被动关闭的一端的状态,发送FIN后的状态(断开连接时第三次握手完成);CLOSED:连接彻底断开;复制代码
TCP/IP协议族诞生之后,各个平台(Window, Unix)就按照此协议规范在系统层面为开发网络程序提供了统一的接口——Socket。通过这个面向传输层协议的系统接口,我们可通过TCP/UDP协议快速实现网络数据的交换,同时也可用来实现应用层协议,如HTTP, SSL等。
Socket(套接字)
Socket是操作系统为上层应用实现网络数据交换提供的接口,我们可通过以下场景来理解:
当你给别人打电话的时候首先要确认打给谁,其次确认打哪个号码,通过这两个条件就可准确的联系到对方。那么在网络中传输数据也是同样的道理,在网络中定位主机是通过IP来实现的,一个IP代表了一台主机,但是每台主机有很多个端口号,所以要准确地与某个应用进行数据交换,除了IP地址外,还需要一个端口号。有了这两个条件,就可通过Socket实现数据交换。由此可见,Socket其实就相当于一部手机,两部手机之间建立一条通路即可实现通话。
Socket编程的步骤
客户端
- 创建scoket;
- 连接服务器;
- 发送、接收数据;
- 关闭socket连接
代码实现(Linux C编程):
#include#include #include #include #include #include #include #include #include #include int main(int argc, char *argv[]){ int sockfd = 0, n = 0; char recvBuff[1024]; struct sockaddr_in serv_addr; if(argc != 2) { printf("\n Usage: %s \n",argv[0]); return 1; } memset(recvBuff, '0',sizeof(recvBuff)); // 创建socket if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("\n Error : Could not create socket \n"); return 1; } // 设置IP和端口 memset(&serv_addr, '0', sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(5000); if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0) { printf("\n inet_pton error occured\n"); return 1; } // 连接到指定的IP和端口 -> 连接成功后即三次握手完成 if( connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { printf("\n Error : Connect Failed \n"); return 1; } // 读数据 while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0) { recvBuff[n] = 0; if(fputs(recvBuff, stdout) == EOF) { printf("\n Error : Fputs error\n"); } } if(n < 0) { printf("\n Read error \n"); } close(scokfd); return 0;}复制代码
服务端
- 创建socket;
- 绑定IP和端口;
- 监听客户端的连接;
- 接收客户端的连接;
- 发送、接收数据
- 关闭socket连接;
代码实现:(Linux C编程)
#include#include #include #include #include #include #include #include #include #include int main(int argc, char *argv[]){ int listenfd = 0, connfd = 0; struct sockaddr_in serv_addr; char sendBuff[1025]; time_t ticks; // 创建socket listenfd = socket(AF_INET, SOCK_STREAM, 0); memset(&serv_addr, '0', sizeof(serv_addr)); memset(sendBuff, '0', sizeof(sendBuff)); // 绑定IP地址和端口 serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(5000); bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 监听客户端口的连接请求 -> 对应于状态图中的LISTEN状态 listen(listenfd, 10); while(1) { // 接收客户端的请求 -> 与客户端三次握手完成 connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); ticks = time(NULL); snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&ticks)); // 向客户端发送数据 write(connfd, sendBuff, strlen(sendBuff)); // 关闭socket close(connfd); sleep(1); }}复制代码
通过以上代码,我们对socket有了一个简单的认识,同时也了解了数据交换的基本流程。后面会对基于TCP协议的HTTP协议进行一个详细的介绍。
参考资料
《TCP/IP详解 卷一》