TCP/IP网络编程学习笔记(二)套接字类型与协议设置、地址族与数据序列

时间:2019-04-15
本文章向大家介绍TCP/IP网络编程学习笔记(二)套接字类型与协议设置、地址族与数据序列,主要包括TCP/IP网络编程学习笔记(二)套接字类型与协议设置、地址族与数据序列使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、协议、协议族、套接字类型

1.概念

  • 协议,Protocol:计算机之间的通信规则
  • 协议族,Protocol Family,头文件sys/socket.h中声明的协议族如下:
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX

IPX Novell协议族

  • 套接字类型:套接字的数据传输方式,有两种:面向连接的套接字(SOCK_STREAM),面向消息的套接字(SOCK_DGRAM)

2.面向连接的套接字(SOCK_STREAM)

可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字

  • 传输过程中数据不会消失
  • 按序传输数据
  • 传输的数据不存在数据边界(收发数据的套接字内部有 buffer,传输数据的计算机通过3次调用 write 函数传递了100字节的数据,但接收数据的计算机可能通过1次 read 函数调用就接收了全部100个字节,也可能多次调用)
// tcp套接字
int tcp_socket = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);

3.面向消息的套接字(SOCK_DGRAM)

不可靠的、不按序传递的、以数据的高速传输为目的的套接字

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 传输的数据有数据边界(接收数据的次数与传输次数相同)
  • 限制每次传输的数据大小
// udp套接字
int udp_socket = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);

4.验证数据边界代码

  • 服务器端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
 
void error_handling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
 
int main(int argc, char* argv[]) {
    int serv_sock;	// 服务器端套接字描述符
    int clnt_sock;	// 客户端套接字描述符
    struct sockaddr_in serv_addr;    // 保存服务器端地址信息
    struct sockaddr_in clnt_addr;    // 保存客户端地址信息
 
    char message[] = "Hello world!";
 
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    // 调用 socket 函数创建套接字
    // 此时的套接字不马上区分服务器端和客户端,若后面调用 bind、listen 函数,则成为服务器端套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");
  
    // 将 serv_addr 所指向的内存空间初始化为0
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;	// 指定地址族
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// IP地址初始化,利用常数 INADDR_ANY 自动获取服务器端的计算机IP地址
    serv_addr.sin_port = htons(atoi(argv[1]));	// 端口号初始化
 
    // 调用 bind 函数分配 IP地址和端口号
    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
 
    // 调用 listen 函数将套接字转为可接收连接状态
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
 
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    // 调用 accept 函数受理连接请求
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");
 
    // 调用 write 函数向客户端传递数据
    write(clnt_sock, message, sizeof(message));
 
    close(clnt_sock);	// 关闭客户端套接字
    close(serv_sock);	// 关闭服务器端套接字
    return 0;
}
  • 客户端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
 
void error_handling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
 
int main(int argc, char* argv[]) {
    int sock;	// 客户端套接字描述符
    struct sockaddr_in serv_addr;
    char message[30];
 
    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
 
    // 调用 socket 函数创建套接字
    // 此时的套接字并不马上分为服务器端和客户端,若后面调用 connect 函数,则成为客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("scoket() error");
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;	// 指定地址族
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);	// IP地址初始化
    serv_addr.sin_port = htons(atoi(argv[2]));	// 端口号初始化
 
    // 调用 connect 函数向服务器端发出连接请求
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");
 
    int str_len = 0;
    // 调用 read 函数获取服务器端传输来的数据
    while (read_len = read(sock, &message[idx++], 1)) {
        if (read_len == -1)
            error_handling("read() error");
        str_len += read_len;
    }
 
    printf("Message from server:%s \n", message);
    printf("Function read call count: %d \n",str_len);
    
    close(sock);	// 关闭客户端套接字
    return 0;
}


  • 运行结果:客户端调用了13次read()函数,说明TCP传输不存在数据边界

二、IP地址、地址族

1.概念

  • 网络协议,IP,Internet Protocol,有IPv4(4字节)IPv6(16字节)两类
  • 网络地址,Internet Address
  • 主机地址,Host Address
  • 回送地址127.0.0.1

2.IPv4

  • 分为ABCDE五类
  • 网络地址+主机地址
  • 网络地址分类与主机地址边界:A类地址的首字节范围:0~127,首位以0开始;B类地址的首字节范围:128~191,前两位以10开始;C类地址的首字节范围:192~223,前三位以110开始

3.用于区分套接字的端口号

  • 16位0~65535,其中0~1023Well-known PORT,不可使用
  • 同种协议套接字的端口号不允许重复,TCP套接字和UDP套接字不会公用端口号,允许重复

4.地址信息的表示

struct sockaddr_in{
    sa_family_t sin_family;    // 地址族
    uint16_t sin_prot;    // 16位TCP/UDP端口号,以网络字节序保存
    struct in_addr sin_addr;    // 32位IPv4地址,以网络字节序保存
    char sin_zeros[8];    // 不使用,只是为使sockaddr_in结构体的大小与sockaddr结构体保持一致而插入的成员
}

struct in_addr{
    in_addr_t s_addr;    // 32位IPv4地址
}

5.字节序

  • 大端序:高位字节存放在低位地址(从左到右,从低到高)
  • 小端序:高位字节存放在高位地址(从右到左,从低到高)
  • 网络字节序:统一为大端序!!!
  • Intel和AMD系列的CPU都采用小端序,小端序系统传输数据时应先转换为大端序排列方式。
  • 除了向 sockaddr_in 结构体变量填充数据外,其他情况无需考虑字节序问题
  • 利用常数 INADDR_ANY 分配服务器端的IP地址,则可自动获取运行服务器端的计算机的IP地址

6.字节序转换函数

#include<arpa/inet.h>
// h表示 host,n表示 network,s表示 short,l表示 long
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
#include<stdio.h>
#include<arpa/inet.h>

int main(int argc, char *argv[]) {
    unsigned short host_port = 0x1234;
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;

    net_port = htons(host_port);
    net_addr = htonl(host_addr);

    printf("Host ordered port: %#x \n", host_port);
    printf("Network ordered port: %#x \n", net_port);
    printf("Host ordered address: %#lx \n", host_addr);
    printf("Network ordered address: %#lx \n", net_addr);

    return 0;
}

运行结果:

Host ordered port: 0x1234 
Network ordered port: 0x3412 
Host ordered address: 0x12345678 
Network ordered address: 0x78563412 

6.IP地址字符串(点分十进制表示法)与32位整型值的相互转换

#include<arpa/inet.h>
in_addr_t inet_addr(const char* cstr);    // 成功时返回 32 位大端序整型值,失败时返回 INADDR_NONE
#include<arpa/inet.h>
int inet_aton(const char* cstr,struct in_addr* addr);    // 成功时返回 1,失败时返回 0
#include<apra/inet.h>
char* inet_ntoa(struct in_addr addr);    // 成功时返回转换的字符串地址,失败时返回-1
// 调用时需十分小心,因为该函数返回指针,但并未向程序员要求分配内存,所以调用完需将字符串复制到其他内存空间,否则下一次调用可能覆盖上次调用的结果
#include<stdio.h>
#include<arpa/inet.h>

int main(int argc, char* argv[]) {
    char *addr[2] = { "1.2.3.4","1.2.3.256" };
    unsigned long conv_addr;

    for (int i = 0; i < 2; i++) {
        conv_addr = inet_addr(addr[i]);
        if (conv_addr == INADDR_NONE) {
            printf("Invalid IP!\n");
        }
        else {
            printf("Network ordered integer addr: %#lx \n", conv_addr);
        }
    }

    return 0;
}

运行结果:

Network ordered integer addr: 0x4030201 
Invalid IP! 

#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>

int main(int argc, char* argv[]) {
    const char* addr[2] = { "1.2.3.4","1.2.3.256" };
    struct sockaddr_in addr_inet;

    for (int i = 0; i < 2; i++) {
        if (!inet_aton(addr[i], (struct in_addr*)&addr_inet.sin_addr)) {
            fputs("Conversion error!", stderr);
            fputc('\n', stderr);
            exit(1);
        }
        else {
            printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
        }
    }

    return 0;
}

运行结果:

Network ordered integer addr: 0x4030201 
Conversion error! 

#include<stdio.h>
#include<string.h>
#include<arpa/inet.h>

int main(int argc, char* argv[]) {
    struct sockaddr_in addr1, addr2;
    char* str_ptr;
    char str_arr[20];

    addr1.sin_addr.s_addr = htonl(0x1020304);
    addr2.sin_addr.s_addr = htonl(0x1010101);

    str_ptr = inet_ntoa(addr1.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation1: %s \n", str_ptr);

    inet_ntoa(addr2.sin_addr);
    printf("Dotted-Decimal notation1: %s \n", str_ptr);
    printf("Dotted-Decimal notation2: %s \n", str_arr);

    return 0;
}

运行结果:

Dotted-Decimal notation1: 1.2.3.4 
Dotted-Decimal notation1: 1.1.1.1 
Dotted-Decimal notation2: 1.2.3.4 

参考书籍:《TCP/IP网络编程》尹圣雨 著,金果哲 译