Skip to content

Linux 网络编程

目录

TCP/IP 协议概述

OSI 参考模型及 TCP/IP 参考模型

TCP/IP 协议族

TCP 和 UDP

TCP

UDP

emm

地址结构相关处理

下面首先介绍两个重要的数据类型:sockaddr 和 sockaddr_in,这两个结构类型都是用来保存 socket 信息的,如下所示:

c
struct sockaddr
{
 unsigned short sa_family; /*地址族*/
 char sa_data[14]; /*14 字节的协议地址,包含该socket 的IP 地址和端口号。*/
};
struct sockaddr_in
{
 short int sa_family; /*地址族*/
 unsigned short int sin_port; /*端口号*/
 struct in_addr sin_addr; /*IP 地址*/
 unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};

sa_family 常见字段

结构定义头文件 #include <netinet/in.h> sa_family

  • AF_INET:IPv4 协议
  • AF_INET6:IPv6 协议
  • AF_LOCAL:UNIX 域协议
  • AF_LINK:链路地址协议
  • AF_KEY:密钥套接字(socket)

数据存储优先顺序

计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式,PC 机通常采用小端模式)。Internet 上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了 4 个函数:htons()、ntohs()、htonl()和 ntohl()。这 4 个地址分别实现网络字节序和主机字节序的转化,这里的 h 代表 host,n 代表 network,s 代表 short,l 代表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表。

uint16_t htons(unit16_t host16bit) uint32_t htonl(unit32_t host32bit) uint16_t ntohs(unit16_t net16bit) uint32_t ntohs(unit32_t net32bit)

地址格式转化

通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制 IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在 IPv4 中用到的函数有 inet_aton()、inet_addr()和 inet_ntoa(),而 IPv4 和 IPv6 兼容的函数有 inet_pton()和 inet_ntop()。由于 IPv6 是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容 IPv4 和 IPv6,但在具体举例时仍以 IPv4 为例。

这里 inet_pton()函数是将点分十进制地址映射为二进制地址,而 inet_ntop()是将二进制地址映射为点分十进制地址

名字地址转化

通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达 128 位,那时就更加不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选择。在 Linux 中,同样有一些函数可以实现主机名和地址的转化,最为常见的有 gethostbyname()、gethostbyaddr()和 getaddrinfo()等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。其中 gethostbyname()是将主机名转化为 IP 地址,gethostbyaddr()则是逆操作,是将 IP 地址转化为主机名,另外 getaddrinfo()还能实现自动识别 IPv4 地址和 IPv6 地址

socket

img

socket() | 创建套接字

c
#include <sys/socket.h>

/**
 * @brief 创建套接字
 * @param family 协议族
 * - AF_INET IPv4
 * - AF_INET6 IPv6
 * - AF_LOCAL UNIX 域协议
 * - AF_ROUTE 路由套接字
 * - AF_KEY 密钥套接字
 * @param type 套接字类型
 * - SOCK_STREAM 流式套接字
 * - SOCK_DGRAM 数据报套接字
 * - SOCK_RAW 原始套接字
 * @param protocol 协议: 通常为 0(原始套接字除外)
 * @return int 套接字描述符 | -1 失败
 */
int socket(int family, int type, int protocol);

bind() | 分配套接字地址

c
#include <sys/socket.h>

/**
 * @brief 分配套接字地址
 * @param sockfd 套接字描述符
 * @param my_addr 本地地址结构
 * @param addrlen 地址结构长度
 * @return int 0 成功 | -1 失败
 */
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

listen() | 监听连接

c
#include <sys/socket.h>

/**
 * @brief 监听连接
 * @param sockfd 套接字描述符
 * @param backlog 请求队列中允许的最大请求数,大多数系统缺省值为 5
 * @return int 0 成功 | -1 失败
 */
int listen(int sockfd, int backlog);

accept() | 接受连接

c
#include <sys/socket.h>

/**
 * @brief 接受连接
 * @param sockfd 套接字描述符
 * @param addr 客户端地址
 * @param addrlen 地址结构长度
 * @return int 0 成功 | -1 失败
 */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

connect() | 发起连接

c
#include <sys/socket.h>

/**
 * @brief 发起连接
 * @param sockfd 套接字描述符
 * @param servaddr 指向数据结构 sockaddr 的指针
 * @param addrlen 地址结构长度
 * @return int 0 成功 | -1 失败
 */
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

close() | 关闭连接

c
#include <unistd.h>

/**
 * @brief 关闭连接
 * @param sockfd 套接字描述符
 * @return int 0 成功 | -1 失败
 */
int close(int sockfd);

send() | 发送数据

c
#include <sys/socket.h>

/**
 * @brief 发送数据
 * @param sockfd 套接字描述符
 * @param msg 发送缓冲区
 * @param len 发送数据长度
 * @param flags 标志位
 * @return int 成功发送的字节数 | -1 失败
 */
ssize_t send(int sockfd, const void *msg, size_t len, int flags);

recv() | 接收数据

c
#include <sys/socket.h>

/**
 * @brief 接收数据
 * @param sockfd 套接字描述符
 * @param buf 接收缓冲区
 * @param len 接收数据长度
 * @param flags 标志位,通常为 0
 * @return int 成功接收的字节数 | -1 失败
 */
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sendto() | 发送数据

c
#include <sys/socket.h>

/**
 * @brief 发送数据
 * @param sockfd 套接字描述符
 * @param msg 发送缓冲区
 * @param len 发送数据长度
 * @param flags 标志位
 * @param to 目标地址
 * @param tolen 地址结构长度
 * @return int 成功发送的字节数 | -1 失败
 */
ssize_t sendto(int sockfd, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

recvfrom() | 接收数据

c
#include <sys/socket.h>

/**
 * @brief 接收数据
 * @param sockfd 套接字描述符
 * @param buf 接收缓冲区
 * @param len 接收数据长度
 * @param flags 标志位,通常为 0
 * @param from 源地址
 * @param fromlen 地址结构长度
 * @return int 成功接收的字节数 | -1 失败
 */
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

EG

该实例分为客户端和服务器端两部分,其中服务器端首先建立起 socket,然后与本地端口进行绑定,接着就开始接收从客户端的连接请求并建立与它的连接,接下来,接收客户端发送的消息。客户端则在建立 socket 之后调用 connect()函数来建立连接。

服务端的代码如下所示:

c
/*server.c*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int sin_size, recvbytes;
    int sockfd, client_fd;
    char buf[BUFFER_SIZE];

    /*建立 socket 连接*/
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    printf("Socket id = %d\n", sockfd);

    /*设置 sockaddr_in 结构体中相关参数*/
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);

    int i = 1; /* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));

    /*绑定函数 bind()*/
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
             sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    printf("Bind success!\n");

    /*调用 listen()函数,创建未处理请求的队列*/
    if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("Listening....\n");

    /*调用 accept()函数,等待客户端的连接*/
    if ((client_fd = accept(sockfd,
                            (struct sockaddr *)&client_sockaddr, &sin_size)) == -1)
    {
        perror("accept");
        exit(1);
    }
    /*调用 recv()函数接收客户端的请求*/
    memset(buf, 0, sizeof(buf));
    if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) == -1)
    {
        perror("recv");
        exit(1);
    }
    printf("Received a message: %s\n", buf);
    close(sockfd);
    exit(0);
}

客户端的代码如下所示:

c
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc, char *argv[])
{
    int sockfd, sendbytes;
    char buf[BUFFER_SIZE];
    struct hostent *host;
    struct sockaddr_in serv_addr;

    if (argc < 3)
    {
        fprintf(stderr, "USAGE: ./client Hostname(or ip address) Text\n");
        exit(1);
    }

    /*地址解析函数*/
    if ((host = gethostbyname(argv[1])) == NULL)
    {
        perror("gethostbyname");
        exit(1);
    }

    memset(buf, 0, sizeof(buf));
    sprintf(buf, "%s", argv[2]);

    /*创建 socket*/
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }

    /*设置 sockaddr_in 结构体中相关参数*/
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
    bzero(&(serv_addr.sin_zero), 8);

    /*调用 connect 函数主动发起对服务器端的连接*/
    if (connect(sockfd, (struct sockaddr *)&serv_addr,
                sizeof(struct sockaddr)) == -1)
    {
        perror("connect");
        exit(1);
    }

    /*发送消息给服务器端*/
    if ((sendbytes = send(sockfd, buf, strlen(buf), 0)) == -1)
    {
        perror("send");
        exit(1);
    }
    close(sockfd);
    exit(0);
}

在运行时需要先启动服务器端,再启动客户端。

shell
$ ./server
Socket id = 3
Bind success!
Listening....
Received a message: Hello,Server!
$ ./client localhost(或者输入 IP 地址) Hello,Server!

网络高级编程

在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如 connet()、recv()和 send()等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多路复用的情况了。本节给出了两种解决 I/O 多路复用的解决方法,这两个函数都是之前学过的 fcntl()和 select()(请读者先复习第 6 章中的相关内容)。可以看到,由于在 Linux 中把 socket 也作为一种特殊文件描述符,这给用户的处理带来了很大的方便

fcntl() | 设置文件描述符状态标志

函数 fcntl()针对 socket 编程提供了如下的编程特性。

  • 非阻塞 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_NONBLOCK。
  • 异步 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_ASYNC。

下面是用 fcntl()将套接字设置为非阻塞 I/O 的实例代码:

c
/* net_fcntl.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 1234
#define MAX_QUE_CONN_NM 5
#define BUFFER_SIZE 1024
int main()
{
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int sin_size, recvbytes, flags;
    int sockfd, client_fd;
    char buf[BUFFER_SIZE];

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1; /* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
             sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }
    if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("Listening....\n");
    /* 调用 fcntl()函数给套接字设置非阻塞属性 */
    flags = fcntl(sockfd, F_GETFL);
    if (flags < 0 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0)
    {
        perror("fcntl");
        exit(1);
    }

    while (1)
    {
        sin_size = sizeof(struct sockaddr_in);
        if ((client_fd = accept(sockfd,
                                (struct sockaddr *)&client_sockaddr, &sin_size)) < 0)
        {
            perror("accept");
            exit(1);
        }
        if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) < 0)
        {
            perror("recv");
            exit(1);
        }
        printf("Received a message: %s\n", buf);
    } /*while*/

    close(client_fd);
    exit(1);
}

// $ ./net_fcntl
// Listening....
// accept: Resource temporarily unavailable

可以看到,当 accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回。

select() | I/O 多路复用

使用 fcntl()函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源的占用。在这里可以使用 select()函数来解决这个问题,同时,使用 select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用 select()函数的服务器端源代码。客户端程序基本上与 10.2.3 小节中的例子相同,仅加入一行 sleep()函数,使得客户端进程等待几秒钟才结束。

c
/* net_select.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define MAX_QUE_CONN_NM 5
#define MAX_SOCK_FD FD_SETSIZE
#define BUFFER_SIZE 1024
int main()
{
    struct sockaddr_in server_sockaddr, client_sockaddr;
    int sin_size, count;
    fd_set inset, tmp_inset;
    int sockfd, client_fd, fd;
    char buf[BUFFER_SIZE];

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(1);
    }
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_sockaddr.sin_zero), 8);
    int i = 1; /* 允许重复使用本地地址与套接字进行绑定 */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
    if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
             sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        exit(1);
    }

    if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
    {
        perror("listen");
        exit(1);
    }
    printf("listening....\n");
    /*将调用 socket()函数的描述符作为文件描述符*/
    FD_ZERO(&inset);
    FD_SET(sockfd, &inset);
    while (1)
    {
        tmp_inset = inset;
        sin_size = sizeof(struct sockaddr_in);
        memset(buf, 0, sizeof(buf));
        /*调用 select()函数*/
        if (!(select(MAX_SOCK_FD, &tmp_inset, NULL, NULL, NULL) > 0))
        {
            perror("select");
        }
        for (fd = 0; fd < MAX_SOCK_FD; fd++)
        {
            if (FD_ISSET(fd, &tmp_inset) > 0)
            {
                if (fd == sockfd)
                { /* 服务端接收客户端的连接请求 */
                    if ((client_fd = accept(sockfd,
                                            (struct sockaddr *)&client_sockaddr, &sin_size)) == -1)
                    {
                        perror("accept");
                        exit(1);
                    }
                    FD_SET(client_fd, &inset);
                    printf("New connection from %d(socket)\n", client_fd);
                }
                else /* 处理从客户端发来的消息 */
                {
                    if ((count = recv(client_fd, buf, BUFFER_SIZE, 0)) > 0)
                    {
                        printf("Received a message from %d: %s\n",
                               client_fd, buf);
                    }
                    else
                    {
                        close(fd);
                        FD_CLR(fd, &inset);
                        printf("Client %d(socket) has left\n", fd);
                    }
                }
            } /* end of if FD_ISSET*/
        }     /* end of for fd*/
    }         /* end if while while*/
    close(sockfd);
    exit(0);
}

运行该程序时,可以先启动服务器端,再反复运行客户端程序(这里启动两个客户端进程)即可,服务器端运行结果如下所示:

shell
$ ./server
listening....
New connection from 4(socket) /* 接受第一个客户端的连接请求*/
Received a message from 4: Hello,First! /* 接收第一个客户端发送的数据*/
New connection from 5(socket) /* 接受第二个客户端的连接请求*/
Received a message from 5: Hello,Second! /* 接收第二个客户端发送的数据*/
Client 4(socket) has left /* 检测到第一个客户端离线了*/
Client 5(socket) has left /* 检测到第二个客户端离线了*/
$ ./client localhost Hello,First! & ./client localhost Hello,Second

Copyright © 2022 田园幻想乡 浙ICP备2021038778号-1