网络编程之理论篇

4,571

网络通信作为互联网的技术支持,已被广泛应用在软件开发中,无论是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?

没错,从小明开始问三次握手到说握手完成就是三次握手的过程,具体的过程如下:

  1. 客户端发向服务端发送一个SYN段,以告知客户端要连接服务端的指定端口;
  2. 服务端收到客户端的报文后,返回一个对客户端报文进行确认的ack段(SYN+1)和一个表示服务端报文的SYN段;
  3. 客户端收到服务端发送的报文后,对服务端发送一个ack(服务端的SYN+1),至此,客户端和服务端就建立了一条连接。

为什么需要3次握手而不是2次呢?

因为客户端收到服务端的应答后,知道连接已建立成功,但是服务端并不知道自己发送的确认报文客户端是否收到,
所以需要客户端对服务端的报文进行确认。

服务端一定能收到客户端发送的确认报文么?

不一定,如果收不到,那么连接就不会建立,所以,3次握手只是理论上确保建立连接的次数。那能否通过4次握手呢?
不行,再握下去就是鸡生蛋,蛋生鸡的问题了。

断开连接

TCP断开连接时需要四次握手,为什么需要4次呢?这是由于TCP半关闭的性质造成的。所谓半关闭,就是可以发送数据,却不能接收数据或只能接收数据,不能发送数据。

四次握手的过程:(由于主动断开连接可发送在客户端,也可发送在服务端,所以下面以A,B来区分两端)

  1. A端向B端发送一个FIN, 告知A端即将断开连接;
  2. B端收到A端发送的FIN后发送一个ack对其进行确认;
  3. 随后B端向再A端发送一个FIN,以告知B端也即将断开连接;
  4. 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编程的步骤

客户端

  1. 创建scoket;
  2. 连接服务器;
  3. 发送、接收数据;
  4. 关闭socket连接

代码实现(Linux C编程):

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

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 <ip of server> \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;
}

服务端

  1. 创建socket;
  2. 绑定IP和端口;
  3. 监听客户端的连接;
  4. 接收客户端的连接;
  5. 发送、接收数据
  6. 关闭socket连接;

代码实现:(Linux C编程)

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h> 

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);
     }
}

代码出处 https://www.thegeekstuff.com/2011/12/c-socket-programming/

通过以上代码,我们对socket有了一个简单的认识,同时也了解了数据交换的基本流程。后面会对基于TCP协议的HTTP协议进行一个详细的介绍。

参考资料

TCP/IP协议族

传输控制协议

《TCP/IP详解 卷一》