网络编程:1. 基础API

Chapter 5

socket编程

主机字节序和网络字节序

大端字节序:高地址存储在低地址

在这里插入图片描述

小端字节序:低地址存储在低地址

在这里插入图片描述

查看自己主机是否是小端编址(两种方法,实际上都是看数据在内存中的存储方式)

#include <stdio.h>

void byteorder() {
    union 
    {
        short value;
        char union_bytes[sizeof(short)];
    } byteorder;

    byteorder.value = 0x0102;
    if ((byteorder.union_bytes[0] == 1) && byteorder.union_bytes[1] == 2) {
        printf("big endian\n");
    } else if (byteorder.union_bytes[0] == 2 && byteorder.union_bytes[1] == 1) {
        printf("little endian\n");
    } else {
        printf("unknow data format\n");
    }

    int num = 0x12345678;
    // &num 取num的地址, (char*)(&num) 将其强转为char *类型的指针,最后取最低位
    char ch = *((char*)(&num));
    if (ch == 0x78) {
        printf("little endian\n");
    } else if (ch == 0x12) {
        printf("big endian\n");
    } else {
        printf("unknow data format\n");
    }
}

int main() {
    byteorder();
    return 0;
}

现在大部分的计算机都是小端字节序,但是仍有部分计算机是使用大端字节序的,所以为了防止出现不同机器字节序导致的数据错乱问题,规定数据传输的时候使用大端字节序,所以如果自己计算机为小端字节序的话,则需要对其进行转换。

#include <sys/socket.h>
// 表示host ot net long host为小端,net为小端
// 转换端口 
uint16_t htons(uint16_t hostshort); // 主机字节序 => 网络字节序 
uint16_t ntohs(uint16_t netshort); // 主机字节序 => 网络字节序 
// 转IP 
uint32_t htonl(uint32_t hostlong); // 主机字节序 => 网络字节序 
uint32_t ntohl(uint32_t netlong); // 主机字节序 => 网络字节序
#include <arpa/inet.h>
// 将点分十进制字符串的IPV4地址,转换为网络字节序的IPV4地址,失败返回-1
in_addr_t inet_addr(const char* strptr);
// 网络地址与点分十进制相互转换
// 函数返回的是一个静态变量地址值,多次调用会导致被覆盖。
char *inet_ntoa(in_addr _in);
int inet_aton(in_addr* _inp);

一个小小的演示案例

#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void big_small() {

    int port = 0x1234;
    uint16_t port_net = htons(port);

    printf("origin port : 0x%x, net port : 0x%x\n", port, port_net);
    in_addr addr1, addr2;
    uint32_t l1, l2;
    l1 = inet_addr("127.0.0.1");
    l2 = inet_addr("192.168.126.126");
    memcpy(&addr1, &l1, sizeof addr1);
    memcpy(&addr2, &l2, sizeof addr2);
    // inet_ntoa返回的是静态字符串,多次调用会被覆盖
    printf("l1 = %s , l2 = %s\n", inet_ntoa(addr1), inet_ntoa(addr2));
}

int main() {
    big_small();

    return 0;
}

输出为:

origin port : 0x1234, net port : 0x3412
l1 = 127.0.0.1 , l2 = 127.0.0.1

为什么 l1 和 l2 都是127.0.0.1 呢?在printf() 调用的参数是从右边往左边执行的,以为addr1的地址将addr2的地址覆盖了,所以我们才看到 l1 l2 都是 127.0.0.1

socket 简介
  • socket地址其实是一个结构体封装端口号和IP等信息
  • 后面的socket相关的api中需要使用到这个 socket地址
通用socket地址
  • socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下

    #include <bits/socket.h> 
    struct sockaddr { 
        sa_family_t sa_family; 
        char sa_data[14]; 
    };
    
    typedef unsigned short int sa_family_t;
    
  • sa_family

    • sa_family 成员是地址族类型(sa_family_t)的变量
    • 地址族类型通常与协议族类型对应
    • PF_*AF_* 都定义在 bits/socket.hsys/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用
    协议族地址族描述
    PF_UNIXAF_UNIXUNIX本地域协议族
    PF_INETAF_INETTCP/IPv4协议族
    PF_INET6AF_INET6TCP/IPv6协议族
  • sa_data

    • sa_data 成员用于存放 socket 地址值,不同的协议族的地址值具有不同的含义和长度
    协议族地址值含义和长度
    PF_UNIX文件的路径名,长度可达到108字节
    PF_INET16 bit 端口号和 32 bit IPv4 地址,共 6 字节
    PF_INET616 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节
    • 由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的

      #include <bits/socket.h> 
      struct sockaddr_storage { 
          sa_family_t sa_family; 
          unsigned long int __ss_align; 
          char __ss_padding[ 128 - sizeof(__ss_align) ]; 
      };
      
      typedef unsigned short int sa_family_t;
      
专用socket地址
简介
  • 很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型

  • 不同socket地址对比图

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZPVpM5t-1665396573314)(D:\Document\学习笔记\pics\sock.png)]

    所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可)(struct sockaddr*) sockaddr_in,因为所有 socket 编程接口使用的地址参数类型都是 sockaddr

UNIX 本地域协议族
#include <sys/un.h> 
struct sockaddr_un { 
    sa_family_t sin_family; 
    char sun_path[108]; 
};
TCP/IP协议族
// IPV4
#include <netinet/in.h> 
struct sockaddr_in { 
    sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */ 
    in_port_t sin_port; /* Port number. */ 
    struct in_addr sin_addr; /* Internet address. */ 
    /* Pad to size of `struct sockaddr'. */ 
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; 
};

struct in_addr { 
    in_addr_t s_addr; 
};

// IPV6
struct sockaddr_in6 { 
    sa_family_t sin6_family; 
    in_port_t sin6_port; /* Transport layer port # */ 
    uint32_t sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr; /* IPv6 address */ 
    uint32_t sin6_scope_id; /* IPv6 scope-id */ 
};

// 相关定义
typedef unsigned short uint16_t; 
typedef unsigned int uint32_t; 
typedef uint16_t in_port_t; 
typedef uint32_t in_addr_t; 
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
相关函数
#include <sys/types.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略(因为已经包含上面两个)int socket(int domain, int type, int protocol);

int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • int socket(int domain, int type, int protocol);

    • 功能:创建一个套接字
    • 参数:
      • domain:协议族(常用如下)
        • AF_INETipv4
        • AF_INET6 :ipv6
        • AF_UNIX, AF_LOCAL:本地套接字通信(进程间通信)
      • type:通信过程中使用的协议类型
        • SOCK_STREAM : 流式协议
        • SOCK_DGRAM : 报式协议
      • protocol:具体的一个协议,一般写0,用于指定type参数的默认协议类型
        • SOCK_STREAM : 流式协议默认使用 TCP
        • SOCK_DGRAM : 报式协议默认使用 UDP
    • 返回值
      • 成功:返回文件描述符,操作的就是内核缓冲区
      • 失败:-1
  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    • 功能:绑定,将fd 和本地的IP和端口进行绑定
    • 参数:
      • sockfd:通过socket函数得到的文件描述符
      • addr:需要绑定的socket地址,这个地址封装了本地的ip和端口号的信息
      • addrlen:第二个参数结构体占的内存大小
    • 返回值:成功:0,失败:-1
  • int listen(int sockfd, int backlog);

    • 功能:监听这个socket上的连接

    • 参数:

      • sockfd:通过socket()函数得到的文件描述符

      • backlog:未连接的和已经连接的和的最大值,可用cat /proc/sys/net/core/somaxconn查看Linux设置值,一般指定5就可以

      在这里插入图片描述

    • 返回值:成功:0,失败:-1

  • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    • 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
    • 参数:
      • sockfd : 用于监听的文件描述符
      • addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
      • addrlen : 指定第二个参数的对应的内存大小
    • 返回值:
      • 成功:用于通信的文件描述符
      • 失败:-1
  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    • 功能: 客户端连接服务器
    • 参数:
      • sockfd : 用于**通信的文件描述符 **
      • addr : 客户端要连接的服务器的地址信息
      • addrlen : 指定第二个参数的对应的内存大小
    • 返回值:成功 0, 失败 -1
通信流程

在这里插入图片描述

  • 服务器端(被动接收连接)

    1. 创建一个用于监听的套接字

      • 监听:监听有客户端的连接
      • 套接字:这个套接字其实就是一个文件描述符
    2. 将这个监听文件描述符本地的IP和端口绑定(IP和端口就是服务器的地址信息)

      • 客户端连接服务器的时候使用的就是这个IP和端口
    3. 设置监听,监听的fd开始工作

    4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)

    5. 通信

      • 接收数据
      • 发送数据
    6. 通信结束,断开连接

  • 客户端

    1. 创建一个用于通信的套接字(fd)

    2. 连接服务器,需要指定连接的服务器的 IP 和 端口

    3. 连接成功了,客户端可以直接和服务器通信

      • 接收数据
      • 发送数据
    4. 通信结束,断开连接

TCP通信实例
服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <netinet/in.h>

#define SERVERIP "127.0.0.1"
#define PORT 8888
#define MAXLEN 4096

int main() {
    // 1. 创建用于监听的套接字
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd != -1);

    // 2. 绑定IP、端口
    sockaddr_in server_addr;
    // 初始化为0
    bzero(&server_addr, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    // 点分十进制转化为网络字节序
    inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr);
    // host to network port
    server_addr.sin_port = htons(PORT);

    int ret = bind(listenfd, (sockaddr*)&server_addr, sizeof server_addr);
    assert(ret != -1);

    // 3. 监听接口
    ret = listen(listenfd, 8);
    assert(ret != -1);

    // 4. 接受客户端连接(被动)
    sockaddr_in client_addr;
    socklen_t client_addr_len= sizeof client_addr;
    int clientfd = accept(listenfd, (sockaddr*)&client_addr, &client_addr_len);

    // 输出客户端信息
    char client_ip[16] = {0};
    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof client_ip);
    uint16_t client_port = ntohs(client_addr.sin_port);
    printf("client ip : %s, port : %d\n", client_ip, client_port);

    // 5. 开始通信交换数据
    char buf[MAXLEN] = {0};
    while(true) {
        ret = read(clientfd, buf, sizeof buf);
        // printf("ret: %d", ret);
        if (ret == -1) {
            perror("recv");
            exit(-1);
        } else if (ret == 0) {
            printf("客户端关闭连接...\n");
        } else if (ret > 0) {
            printf("CLINET %s > %s\n", client_ip, buf);
        }
        char *send_msg = "Welcome, I am server.";
        write(clientfd, send_msg, strlen(send_msg) + 1);
    }
    // 关闭文件描述符
    close(clientfd);
    close(listenfd);

    return 0;
}
客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>

#define SERVERIP "127.0.0.1"
#define PORT 8888
#define MAXLEN 4096

int main () {
    // 1. 创建连接套接字
    int connfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(connfd >= 0);

    // 2. 连接服务器
    sockaddr_in server_addr;
    bzero(&server_addr, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr);
    server_addr.sin_port = htons(PORT);

    int ret = connect(connfd, (sockaddr*)&server_addr, sizeof server_addr);
    assert(ret >= 0);

    // 3. 与服务器通信,交换数据
    char buf[MAXLEN] = {0};
    while(true) {
        char *send_msg = "I am client, Hi\n";
        // sizeof遇到空格会终止
        write(connfd, buf, strlen(buf) + 1);
        sleep(1);
        
        ret = read(connfd, buf, sizeof buf);
        if (ret == -1) {
            perror("read");
            exit(-1);
        } else if (ret == 0) {
            printf("服务器断开连接...\n");
            break;
        } else {
            printf("SERVER >%s\n", buf);
        }
    }
    close(connfd);

    return 0;
}
输出

服务器端

在这里插入图片描述

客户端

在这里插入图片描述

这里发现了一个有意思的现象,client第一次输出的为空,根据TCP三次握手我们可以得知,在client establish之后又发了一次空的数据包,所以第一次输出是空。(此处解释可能有误,如有正解,请指出)
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值