github仓库:https://github.com/brucewayne9064/LinuxCpp_server

套接字编程包括tcp套接字编程,udp套接字编程,原始套接字编程

一、套接字基本概念

套接字是TCP/IP模型中应用层与传输层的中间抽象层,socket编程接口是应用层与传输层之间的编程接口

socket是一种“打开——读/写——关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接后,可以向自己文件写入内容供对方读取或者读取对方的内容,通信结束时关闭文件

二、网络程序的架构

  • browser/server

  • client/server (网络编程中使用的架构)

三、IP地址的格式转换

注意大小端问题

  • IP地址的高低位:

    IP地址是一个32位的二进制数,通常分为四个字节,每个字节用一个十进制数表示,中间用点隔开。例如,127.0.0.1就是一个IP地址。IP地址可以分为网络部分和主机部分,不同的类别的IP地址有不同的划分方式。一般来说,网络部分在IP地址的高位字节中,主机部分在低位字节中。例如,对于A类地址,第一个字节是网络部分,后面三个字节是主机部分;对于B类地址,前两个字节是网络部分,后两个字节是主机部分。

  • 高序字节:

    高序字节是指一个多字节数据中的最高有效位。例如,对于一个四字节的32位整数0x12345678,其高序字节是0x12,低序字节是0x78。

  • 大小端:

    大小端是指数据在存储或者传输时的字节顺序,具体分为大端和小端。大端是指将高序字节存储在起始地址小端是指将低序字节存储在起始地址。例如,对于一个由两个字节组成的16位整数0x1234,在内存中的存储方式如下:

    地址 大端 小端
    0x1000 0x12 0x34
    0x1001 0x34 0x12
  • 网络字节序:

    网络字节序是TCP/IP协议规定的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用大端模式,即数据的高位字节存放在内存的低地址处。

    UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节

  • 主机字节序:

    主机字节序是指某个给定系统所用的字节顺序,它与CPU设计有关,不同的CPU可能有不同的主机字节序。比如x86系列CPU都是小端模式,而Motorola 6800为大端模式。当不同主机字节序的计算机之间进行网络通信时,需要将发送方的主机字节序转换为网络字节序,然后将接收方的网络字节序转换为主机字节序。

  • 点分十进制的IP地址是一种人类可读的表示方式,它并不涉及存储方式的问题。所以直接看是看不出来的

    判断本地是大端还是小端的一种方法是通过强制类型转换截断一个多字节数据,看看低地址处存放的是高位字节还是低位字节

    举例说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>

    int isBigEndian()
    {
    unsigned short test = 0x1234; // 00010010 00110100
    if (*((unsigned char *)&test) == 0x12) // 取低地址处的字节
    return 1; // 大端模式
    else
    return 0; // 小端模式
    }

    int main()
    {
    if (isBigEndian())
    printf("This machine is big endian.\n");
    else
    printf("This machine is little endian.\n");
    return 0;
    }

inet_addr,inet_ntoa,inet_aton这些函数都是用来在IP地址和整数之间进行转换的,但是有一些区别:

  • inet_addr()把一个点分十进制的IP地址转换成一个网络字节序(大端的无符号整数),如果输入无效,返回INADDR_NONE(通常是-1)。
  • inet_ntoa()把一个网络字节序转换成一个点分隔的IP地址
  • inet_aton()**把一个点分隔的IP地址转换成一个网络字节序的二进制数据,并存储在inp指向的结构体中,**如果地址有效,返回非零值,否则返回零。

四、套接字的类型

原始套接字和标准套接字的区别在于原始套接字可以读写内核没有处理的IP数据报,而标准套接字只能读TCP或者UDP报文

  • 流套接字(SOCK_STREAM)

    提供面向连接,可靠的数据传输(因为用了TCP协议)

  • 数据报套接字(SOCK_DGRAM)

    提供无连接服务(因为用了UDP协议)

  • 原始套接字(SOCK_RAW)

    原始套接字允许对较低层次的协议直接访问(IP,ICMP),常用于检验新的协议实现,或者访问现有服务中配置的新设备,或者用于网络监听

五、套接字地址

一个套接字代表通信的一端,每端有一个套接字地址(套接字的一个参数)

socket地址包括:IP地址(从网络中识别主机),端口信息(从主机中识别进程)

socket地址分为:通用socket地址,专用socket地址(自定义专属网络地址)

本地机器上的套接字里保存的套接字地址是对方的还是自己的,取决于套接字的类型和使用场景:

  • 对于流套接字(SOCK_STREAM),通常需要建立连接,所以本地机器上的套接字里保存了对方的套接字地址(IP地址和端口号)
  • 对于数据报套接字(SOCK_DGRAM),通常不需要建立连接,所以本地机器上的套接字里保存了自己的套接字地址(IP地址和端口号)

获取套接字地址:

  • getsockname():获取本地

  • getpeername():获取对端

本地产生套接字地址:

  • 本地套接字通过bind函数获取地址
  • 本地套接字没有绑定地址,但是通过connect函数和远程建立了连接,此时内核会分配一个地址给本地套接字

六、主机字节序和网络字节序

  • htonl()将uint32_t主机字节序转换为网络字节序
  • ntohl()将uint32_t网络字节序转换为主机字节序
  • htons()将uint16_t主机字节序转换为网络字节序
  • ntohs()将uint16_t网络字节序转换为主机字节序

七、协议族和地址族

Linux支持PF和AF,所以指定协议用PF(protocol family),指定地址用AF(address family)

八、TCP套接字编程的基本步骤

服务器端编程的七个步骤一般是:

  • 创建一个套接字(socket函数),用于监听客户端的连接请求。
  • 绑定一个本地地址和端口到套接字上(bind函数),用于标识服务器的身份。
  • 将套接字设置为监听模式(listen函数),用于等待客户端的连接请求,该套接字变成监听套接字
  • 接受一个客户端的连接请求,返回一个新的套接字(accept函数),用于和客户端进行通信。
  • 通过新的套接字,与客户端进行数据交换(send和recv)。关闭与客户端的通信套接字,结束本次会话(closesocket函数)。
  • 监听套接字继续监听,等待其他客户端连接请求。
  • 关闭监听套接字(closesocket函数),结束服务器程序。

客户端编程的四个步骤一般是:

  • 创建一个套接字(socket函数),用于和服务器进行通信。
  • 向服务器发出连接请求(connect函数)。
  • 和服务器通信(send和recv)
  • 关闭客户端,关闭套接字(closesocket)。

九、TCP套接字编程的相关函数

  • 客户端程序一般不调用bind函数来绑定socket地址,而使用socket默认的地址,是因为客户端不需要确定自己的ip和端口,而是由内核根据路由表来选择一个合适的本地地址和临时端口。如果你尝试读取了新创建的socket,里面的ip是0.0.0.0,端口也是0,那么表示这个socket还没有被绑定到任何具体的地址和端口上。当你调用connect或listen函数时,内核会为这个socket分配一个本地地址和临时端口。你可以通过getsockname函数来获取这个分配后的地址和端口。
  • 服务器程序为什么要调用bind函数,是因为服务器需要在一个固定的地址和端口上监听客户端的连接请求。如果服务器不调用bind函数,那么内核会为套接字随机分配一个地址和端口,这样客户端就不知道怎么连接到服务器了。而客户端不需要调用bind函数,是因为客户端是主动向服务器发起连接请求的,内核会为客户端套接字自动分配一个本地地址和临时端口。客户端只需要知道服务器的地址和端口,就可以使用connect函数来建立连接了。

十、简单的TCP套接字编程

十一、深入理解TCP编程

  • 数据收发涉及的缓冲区:

    发送端:应用程序发送缓冲区(程序员开辟),TCP套接字发送缓冲区(内核缓冲区)。调用send函数后,将数据从程序缓冲区拷贝到内核缓冲区,内核缓冲区发送至网络

    接收端:TCP套接字接收缓冲区(内核缓冲区),应用程序接收缓冲区,同样包含两个接收步骤

  • 一次请求响应的数据接收

    接收端接收完全部数据后,接收就算结束,发送端断开连接。recv函数的返回值代表接收的字节数,发送端调用shutdown关闭发送,接收端看到recv的返回值为0,就跳出接收循环。

  • 多次请求响应的数据接收(定长数据的接收)

    判断连接是否结束的方式是通信双方约定好发送数据的长度,接收方提前知道发送的数据长度,接收完固定长度后结束,发送方发送完并不调用shutdown

  • 变长数据的接收

    方法1:数据报结尾增加结束标识符,如果碰到结束标识符,表示结束,需要扫描每个字符

    方法2:在数据报前加一个报头,报头里有一个字段表示消息长度

    扩展结构体:

    1
    2
    3
    4
    struct MyData {
    int nLen;
    char data[0];
    };
    • 可以实现数组的动态扩展,即根据需要分配不同大小的内存空间给结构体。
    • 可以节省内存空间,因为char data[0]不占用结构体的空间,只是一个指向结构体后面数据的地址。
    • 可以方便地通过data访问结构体后面的数据,而不需要额外的指针变量。

    一个简单的例子是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    struct MyData {
    int nLen;
    char data[0];
    };

    int main() {
    char str[10] = "123456789";
    struct MyData *p = (struct MyData *)malloc(sizeof(struct MyData) + strlen(str));
    memcpy(p->data, str, strlen(str));
    printf("p->data is %s\n", p->data);
    free(p);
    return 0;
    }

    输出:

    1
    p->data is 123456789

    可以看到,通过malloc分配了一个大小为sizeof(struct MyData) + strlen(str)的内存空间给p,然后将str拷贝到p->data中,最后通过p->data打印出了str的内容。

十二、I/O控制命令

套接字的IO控制用于设置套接字的工作模式(阻塞式或者非阻塞式),也可以用来获取与套接字相关的IO操作的参数信息(比如读取输入缓冲区的字节数)

十三、套接字选项

设置或者获取套接字的属性