网络socket基础概论

网络概论

一、Socket 分类

1、Internet Sockets

网络中存在很多类型的socket,不过这里我们只讲解两种类型的socket,其中一种是stream sockets (流式 Sockets),另一种是Datagram Sockets(数据包格式)。

1.1、Stream Sockets

Stream socket是指应用程序要传输的数据如水流一般在水管中传输一般,经由这个stream socket(如同水管)流向目的。串流式 socket 是数据会由传输层负责处理遗失、依序送达等工作,以确保应用程序所送出的数据能够可靠且依序到达。

Stream sockets是可靠的、双向连接的通讯串流,其建立在TCP协议上。利用TCP保证数据可以依序抵达而且不会出错。

1.2、Datagram Sockets

Datagram socket 是基于信息导向的方式传输数据,应用程序传输的每段数据会如邮件一般送出,由于传输数据包的路径可能会随着网络条件动态变化,每段数据抵达的顺序不一定会按照送出的顺序抵达,并且数据可能会在传输过程中丢失,而应用程序也无法知道是否传输成功。

Datagram sockets也称为"无连接的sockets",其建议在UDP协议上。

2、网络理论

计算机网络体系结构分为三种:OSI体系结构(七层)、TCP/IP体系结构(四层)、五层体系结构。

2.1 OSI体系结构

OSI,一个著名的分层网络模型,理论完善,概念清楚,但既复杂又不实用。其分成七层,由上到下(按照与上层应用的邻近程度)依次为:

  • 应用层(Application Layer)

网络协议的最高层,定义应用程序(进程)间通信和交互的规则,通过进程间的交互完成特定网络任务

  • 表示层(Presentation Layer)

完成传输数据的加密和压缩操作。

  • 会话层(Session Layer)

网络中会话的开始、恢复、释放与同步

  • 传输层(Transport Layer)

负责为两台终端中的进程提供通信服务,主要包含 TCP(传输控制协议)和UDP(用户数据报协议)。

  • 网络层(Network Layer)

负责为分组网络中的不同主机提供通信服务,并通过选择合适的路由将数据传递到目标主机

  • 数据链路层(Data Link Layer)

通常简称为链路层 ,在两个相邻节点传输数据时,将网络层传输下来的 IP 数据报封装成帧,在两个相邻节点之间的链路上传送帧。

  • 物理层(Physical Layer)

保证数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境。

2.2 TCP/IP体系结构

相较于 OSI 来说,TCP/IP是实际运行的网络协议,其分为四层:

  • 应用层(Application Layer)

包含应用层、表示层和会话层

  • 传输层(Transport Layer)
  • 网络层(Network Layer)
  • 网络接口层(Network Interface Layer)

包含数据链路层和物理层

2.3 五层网络体系结构

五层网络体系结构折中了 OSI 体系结构和 TCP/IP 体系结构,常用于教学。

  • 应用层(Application Layer)

包含应用层、表示层和会话层

  • 传输层(Transport Layer)
  • 网络层(Network Layer)
  • 数据链路层(Data Link Layer)
  • 物理层(Physical Layer)

二、IP 地址结构和数据转换

2、IPv4 和 IPv6

2.1、IPv4
2.1.1 基本概念

IPv4(Internet Procotol Version 4)是1981年发布的第四版互联网协议,采用32位地址 ,可提供约43亿个唯一地址,常用点分十进制表示。

地址示例:127.0.0.1(十进制表示),11111111.00000000.00000000.00000001(二进制表示)。

地址分为四段,每段取值范围在0~255之间。

2.1.2 地址分类

IPv4地址根据网络规模的不同 ,将地址分为A、B、C三类,称为主类网 。并且还定义了用于组播寻址的 D 类地址,以及被保留用于未来使用的 E 类地址。

这五类地址由 IPv4 地址的第一个字节的高位决定

  • A 类地址,首位 bit 为 0,地址范围为 0.0.0.0 ~ 127.255.255.255,私有地址范围为 10.0.0.0 ~ 10.255.255.255。

  • B 类地址,前两位 bit 为 10,地址范围为 128.0.0.0 ~ 192.255.255.255,私有地址范围为 172.16.0.0 ~ 172.31.255.255。

  • C 类地址,前三位 bit 为 110,地址范围为 192.0.0.0 ~ 223.255.255.255,私有地址范围为 192.168.1.0 ~ 192.168.255.255。

  • D 类地址,前四位 bit 为 1110,地址范围为 224.0.0.0 ~ 239.255.255.255。

不标识网络,用于组播寻址

  • E 类地址,前四位 bit 为 1111,地址范围为 240.0.0.0 ~ 255.255.255.255。
2.1.3 IPv4报文格式

IPv4报文格式如下:

其中,每个字段的意义如下:

  • version:版本号,4 表示为 IPv4,6 表示为 IPv6

  • IHL:首部长度,如果不带 option 字段,则为20,最长为 60

  • Type of Service:服务类型,只有在有 Qos 差分服务要求时这个字段才起作用

  • Total Length:总长度,整个 IP 数据报的长度,包括首部长度和数据之和,单位为字节,最大为 65535,总长度必须不超过最大传输单元 MTU

  • Identification:标识,主机每发一个报文就会加1分片重组时会用到该字段

  • Flags:标志位,首位必须为 0(保留);次位为 DF(Don't Fragment)字 - 能否分片位 ,0 表示可以分片,1 表示不能分片;末位为 MF(More Fragment),表示该报文是否为最后一片,0 表示最后一片,1 表示后面还有

  • Frament Offset:片偏移,分片重组时会用到该字段。表示在分片后,该片在原分组中的相对位置。以 8 个字节为偏移单位

  • Time to Live:存活时间,报文在网络中的最大生存时间(最大跳数)

  • Protocol:协议,下一层协议。指出此报文携带的数据使用何种协议,以便目的主机的 IP 层将报文上交给哪个进程处理

  • Header Checksum:首部校验和,只检验报文的首部,不检验数据部分

IPv4 校验和计算过程:

初始化校验和字段 :将校验和字段的值初始化为 0。

分组数据 :将头部中的每一段数据按照 16 位进行分组。

累加数据段 :依次将这些数据段的值累加到校验和累加器中。

处理进位 :如果累加的和超过了 16 位,需进行进位操作,将高 16 位加到低 16 位上。

取反:对累加和的最终结果进行取反,得到校验和的值。

  • Source Address:源 IP 地址

  • Destination Address:目的 IP 地址

  • Options:长度可变 ,选项字段,用来支持排错,测量以及安全等措施

  • Padding:长度可变填充字段,全填0

2.2、IPv6
2.2.1 基本概念

IPv6(Internet Protocol Version 6)是网络层协议的第二层标准协议,其所在的网络层提供了无连接的数据传输服务。其解决了 IPv4 存在的许多不足之处,如:

  • 公有IP地址数量不足
  • 私有地址交流效率低
  • 设备维护的路由表表项数量过大
  • 不易进行自动配置和重新编址
  • 安全溯源困难

IPv6 地址长度为 128 位 ,其表示形式为:X:X:X:X:X:X:X:X。128 位的 IPv6 地址被分为 8 组,每组的 16 位用 4 个十六进制字符(09,AF)来表示,组和组之间用冒号(:)隔开。其中每个 "X" 表示一组十六进制数值,如下:2408:8120:A03E:0000:6719:2104:D756:C012。

2.2.2 地址分类

IPv6 主要有三种地址:

  • 单播地址(Unicast):唯一标识一个接口,发送到单播地址的数据包将被传输到此地址所标识的唯一接口。

  • 任播地址(Anycast):用来标识一组接口,发送到任播地址的数据包被传输给地址所标识的一组接口中距离源节点最近的一个接口。

  • 组播地址(Multicast):用来标识属于不同节点的一组接口,发送的组播地址的数据包被传输给此地址所标识的所有接口。

2.2.3 IPv6 数据报文格式

IPv6报文格式如下:

  • Version:版本号,4 表示为 IPv4,6 表示为 IPv6

  • Traffic Class:流量类别,该字段以区分业务编码点(DSCP)标记一个 IPv6 数据包,以此*指明数据包应当如何处理

  • Flow Label:流标签,该字段用于标记 IP 数据包的一个流

  • Payload Length:该字段表示有效载荷的长度,有效载荷是指紧跟 IPv6 基本报头的数据包,包含 IPv6 扩展头

  • Next Header:下一报头,指明了跟随在 IPv6 基本报头后的扩展报头的信息类型

  • Hop Limit:跳数限制,定义了 IPv6 报文所能经过的最大跳数

  • Source Address:源地址

  • Destination Address:目的地址

  • Extension Headers:扩展报头,

3、字节顺序

网络中存在两种字节序,一种为网络字节序 ,另一种为主机字节序

3.1、大端字节序和小端字节序

网络字节序是大端字节序(Big Endian),最高有效位存于最低内存地址处,最低有效位存于最高内存处。

主机字节序一般为小端字节序(Little Endian)(存在大端字节序的主机字节序),最高有效位存于最高内存地址处,最低有效位存于最低内存处。

  • 判断本机字节序
c 复制代码
#include <stdio.h>

int verifyByteOrder()
{
    int i = 0; // 0x00000001 
    char *p = (char *)&i;
    // 如果读取到的第一个字节为1,则为小端,否则为大端
    return *p == 0;
}


int main()
{
    if (verifyByteOrder() == 1)
    {
        printf("Little Endian\n");
    }
    else
    {
        printf("Big Endian\n");
    }

    return 0;
}
3.2、字节序转换函数

在数据结构struct sockaddr_in中,sin_addrsin_port分别封装在包的 IP 和 UDP 层,因此,需要转换为网络字节顺序 ;而sin_family是被内核使用来决定在数据结构中包含什么类型的地址,因此其需为本机字节序。

c 复制代码
#include <arpa/inet.h>

// h --- host   n --- netowrk  s --- short  l --- long 

// 主机字节序 -> 网络字节序
unit16_t htons(unit16_t host_short);
unit32_t htonl(unit32_t host_long);

// 网络字节序 -> 主机字节序
unit16_t ntohs(unit16_t net_short);
unit32_t ntohl(unit32_t net__long);

4、数据结构

4.1、addrinfo

addrinfo结构主要在网络编程解析 hostname 时使用,其包含在头文件#include<netdb.h>中。其定义如下:

c 复制代码
struct addrinfo{
    int ai_flags; // AI_PASSIVE, AI_CANONNAME 等
    int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
    int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol; // 0 - "any"
    size_t ai_addrlen; // ai_addr 的大小,单位是 byte
    struct sockaddr* ai_addr; // struct sockaddr_in or struct sockaddr_in6
    char *ai_cannonname; // hostname
    struct addrinfo *ai_next; 
};
4.2、sockaddr

sockaddr在头文件#include <sys/socket.h>中定义,其中的sa_data中包含着目标地址和端口信息。

c 复制代码
struct sockaddr{
    unsigned short sa_family;  // 地址簇,如 AF_INET
    char sa_data[14];  // 14字节协议地址
}
4.3、sockaddr_in

sockaddr_in在头文件#include <netinet/in.h>/#include <arap/inet.h>中定义,该结构体把地址信息和端口分开存储在两个变量中。

c 复制代码
struct sockaddr_in{
    short int sin_family;  // 通信类型
    unsigned short int sin_port;  // 端口
    struct in_addr sin_addr;  // Internet 地址
    unsigned char sin_zero[8]; // 与sockaddr结构的长度相同
}

sin_zero用于指向sockaddr,这样一来,即使socket()想要的是 struct sockaddr*,也可以将其转换。

c 复制代码
struct in_addr{
    uint32_t s_addr;
};

5、IP地址处理

假设我们有一个 sockaddr_in 结构体 socket_in,也有一个IP地址 192.168.1.2 或 2001:db8:1:1245::9815 要存储在其中,

我们可以用到函数 inet_pton(),将IP地址从存储到结构体中。同时,也可以使用inet_pton(),将二进制表示的地址转换为字符串。

c 复制代码
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main()
{
    struct sockaddr_in sa;
    struct sockaddr_in6 sa6;

    // 转换为 二进制存储
    if (inet_pton(AF_INET, "192.168.1.2", &(sa.sin_addr)) <= 0)
    {
        perror("INET4");
        exit(1);
    }

    printf("sa.sinaddr: %u\n", sa.sin_addr);

    if (inet_pton(AF_INET6, "2001:db8:1:1245::9815", &(sa6.sin6_addr)) <= 0)
    {
        perror("INET6");
        exit(1);
    }
    printf("sa6.sinaddr: %u\n", sa6.sin6_addr);


    // 转换为 字符串
    char ip4[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
    printf("IPv4 Address: %s\n", ip4);

    char ip6[INET6_ADDRSTRLEN];
    inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
    printf("IPv6 Address: %s\n", ip6);

    return 0;
}

三、基本函数

1、getaddrinfo()

1.1、定义
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node,   // 域名 www.example.com 或者 IP地址 192.168.1.234  为NULL时,创建服务器端socket
                const char *service,  // 协议 http ftp sftp 或者 具体的端口号 443 80 
                const struct addrinfo *hints, // hints 指向一个已经填好相关数据的 struct addrinfo
                struct addrinfo **res)  // 返回一个指向链表的指针 res
1.2、使用示例
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <errno>

struct addrinfo hints, *ai, *p;

hints.ai_family = AF_UNSPEC; // IPv4和IPv6均可,也可指定为 AF_INET/AF_INET6
hints.ai_socktype = SOCK_STREAM; // 
hints.ai_flags = AI_PASSIVE; // 将本机的地址指定给socket structure
int rv;

// 得到指向struct addinfo的链表指针 ai
if ((rv = getaddrinfo(NULL, , &hints, &ai)) != 0)
{
    fprintf(stderr, "getaddrinfo error:%s\n", gai_strerror(rv));
    exit(1);
}

// 遍历链表,建立监听
for (p = ai, p != NULL, p = p->ai_next)
{
    // ....
}

// 释放链表
freeaddrinfo(servinfo);
1.3、应用示范

获取某个网站(如www.baidu.com)的IP地址

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
    struct addrinfo hints, *res, *p;
    int status;
    char ipstr[INET6_ADDRSTRLEN];

    if (argc != 2)
    {
        fprintf(stderr, "usage: showip hostname\n");
        return 1;
    }

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0)
    {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
        return 2;
    }
    printf("IP addresses for %s\n\n", argv[1]);

    for (p = res; p != NULL; p = p->ai_next)
    {
        void *addr;
        char *ipver;

        if (p->ai_family == AF_INET)
        {
            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
            addr = &(ipv4->sin_addr);
            ipver = "IPv4";
    }
        else
        {
            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
            addr = &(ipv6->sin6_addr);
            ipver = "IPv6";
        }

        // 将 IP 转换为 字符串格式
        inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));
        printf(" %s: %s\n", ipver, ipstr);
    }
    freeaddrinfo(res); // 释放链表
    return 0;
}
                    
  • 使用
bash 复制代码
./getaddrinfo_demo www.baidu.com

2、socket()

2.1、定义

在建立连接时,首先需要用 socket()函数 建立套接字。

c 复制代码
#include <sys/types.h>
#include <sys/socket.h>

// domain --- 地址簇,如AF_INET、PF_INET
// type --- 告诉内核是 SOCK_STREAM 还是 SOCK_DGRAM ...
// protocol --- 0
int socket(int domain, int type, int protocol);
2.2、使用示例
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>


struct addrinfo hints, *ai, *p

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
int rv;

// 新增
int listener;
int yes = 1;

if ((rv = getaddrinfo(NULL, , &hints, &ai)) != 0)
{
    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rv));
    exit(1);
}

for (p = ai; p != NULL; p = p->ai_next)
{
    // 新增

    // 获取 文件描述符
    listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (listener < 0)
    {
        continue;
    }

    // ...
}

freeaddrinfo(ai);

3、bind()函数

3.1、定义

在创建完套接字后,要将套接字和机器上的端口号绑定(如果要用listen()来监听端口的数据)。

c 复制代码
#include <sys/types.h>
#include <sys/socket.h>


// 错误时返回 -1,并将 errno 设置为该错误的值
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
3.2、使用示例
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>


struct addrinfo hints, *ai, *p

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
int rv;
int listener;
int yes = 1;

if ((rv = getaddrinfo(NULL, , &hints, &ai)) != 0)
{
    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rv));
    exit(1);
}

for (p = ai; p != NULL; p = p->ai_next)
{

    // 获取 文件描述符
    listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (listener < 0)
    {
        continue;
    }

    // 复用该端口 -- 防止错误 - Address already in use
    setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
    if (bind(listener, p->ai_addr, p->ai_addrlen) < 0)
    {
        close(listener);
        continue;
    }
    // ...
}

// ...

freeaddrinfo(ai);

4、connect()

4.1、定义
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>


// 错误时返回 -1, 并设定 errno 变量
// dst_addr 是保存着目的地端口和地址的 struct sockaddr实例
int connect(int sockfd, struct sockaddr *dst_addr, int addrlen);
使用示例

连接到 www.example.com 的 Port 3490:

c 复制代码
struct addrinfo hints, *ai;
int sockfd;

hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;

getaddrinfo("www.example.com", "3490", &hints, &ai);

// 建立 Socket
sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);

// connect
connect(sockfd, ai->ai_addr, ai->ai_addrlen);

5、listen()

5.1、定义

在服务端,我们需要调用listen()来监听端口,是否有新连接进入,随后使用accept()接收连接。

c 复制代码
// backlog - 队列数量,大多数默认为 20
int listen(int sockfd, int backlog);

在服务端的一般调用顺序为:

c 复制代码
getaddrinfo();
socket();
bind();
listen();
accept();
5.2、使用示例
c 复制代码
// 

if (p == NULL)
{
    fprinf(stderr, "selectserver: failed to bind\n");
    exit(2);
}

if (listen(listener, 10) == -1)
{
    perror("listen");
    exit(3);
}

6、accept()

6.1 定义
c 复制代码
#include <sys/types.h>
#include <sys/socket.h>

// addr -> 一个指向 local struct sockaddr_storage 的指针
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

7、send()和recv()

现在,连接已经建立好了,那么,就需要send()recv()来收发消息了。

7.1 定义
c 复制代码
// 错误时返回 -1,并将 errno 设置为错误码


// flags 一般设置为0
int send(int sockfd, const void *msg, int len, int flags);


// 返回 0 -> 远端关闭连接
int recv(int sockfd, void *buf, int len, int flags);

8、sendto()和recvfrom()

如果使用 unconnected datagram socket,那么就需要sendto()recvfrom()了。

8.1、定义
c 复制代码
// to 是一个指向 struct sockadd 的指针
// tolen 可以设置为 sizeof *to 或 sizeof(struct sockaddr_storage)
// 返回实际传输的数据量,错误返回 -1
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, socklen_t tolen);

// 
// fromlen 一般初始化为 sizeof *from 或 sizeof(struct sockaddr_storage)
// 返回接收的数量量,错误返回 -1
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

9、close()和shutdown()

如果要关闭连接,则可使用close()shutdown()

c 复制代码
close(sockfd);


// how - 0 1 2
// 0 - 不允许再接收数据
// 1 - 不允许再传送数据
// 2 - 不允许接收和传送数据 (close)
// 成功返回 0,失败返回 -1
int shutdown(int sockfd, int how);

10、getpeername()

getpeername()函数会告诉你另一端连接的 stream socket 是谁。

c 复制代码
#include <sys/socket.h>

// 错误时返回 -1,并设置相对的 errno
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

11、gethostname()

gethostname()函数返回本终端的信息。

c 复制代码
#include <unsitd.h>

// 成功返回 0,错误返回 -1,并设置 errno
int gethostname(char *hostname, size_t size);

四、client-server简易实现

接下来,来构建一个简易的STREAM客户端-服务端通信模型。

1、server

  • 代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>


#define PORT "3490"
#define BACKLOG 10


void sigchild_handler(int s)
{
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET)
    {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }
    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}


int main()
{
    // 定义变量和初始化值
    int sockfd, new_fd; // sockfd 是listen,newfd 是新的连接
    struct addrinfo hints, *p, *servinfo;
    struct sockaddr_storage their_addr; // 连接者的地址资料
    socklen_t sin_size;
    struct sigaction sa;
    int yes = 1;
    char s[INET6_ADDRSTRLEN];
    int rv;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    // getaddrinfo

    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
    {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // 循环找到可用的连接地址
    for (p = servinfo; p != NULL; p = p->ai_next)
    {
        // socket - 创建文件描述符
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
        {
            perror("setsockopt");
            exit(1);
        }

        // bind
        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
        {
            close(sockfd); // 防止文件描述符泄露
            perror("bind");
            continue;
        }

        break;
    }

    if (p == NULL)
    {
        fprintf(stderr, "server: failed to bind\n");
        return 2;
    }

    // 释放结构体
    freeaddrinfo(servinfo);

    // listen - 监听该端口
    if (listen(sockfd, BACKLOG) == -1)
    {
        perror("listen");
        exit(1);
    }

    sa.sa_handler = sigchild_handler; // 处理全部死掉的 processes
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;

    if (sigaction(SIGCHLD, &sa, NULL) == -1)
    {
        perror("sigaction");
        exit(1);
    }

    printf("server: waiting for connections...\n");
    while (1)
    {
        sin_size = sizeof(their_addr);

        // accept
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);

        if (new_fd == -1)
        {
            perror("accept");
            continue;
    }

        inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof(s));

        printf("server: got connetion from %s\n", s);

        // 子进程返回 0,父进程返回新创建子进程的PID
        if (!fork())
        {
            close(sockfd); // child process 不需要 listen
            if (send(new_fd, "hello, world!", 13, 0) == -1)
            {
                    perror("send");
            }
            close(new_fd);
            exit(0);

        }
        close(new_fd); // 父进程 不需要 new_fd
    }
    return 0;
}
  • 编译运行
bash 复制代码
# 编译
gcc -o server01_demo server01.c

# 运行
./server01_demo

2、client

  • 代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define PORT "3490" // client 要连接得 port
#define MAXDATASIZE 100 // 可以一次接收得最大字节数


void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET)
    {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }
    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
    int sockfd, numbytes;
    char buf[MAXDATASIZE];
    struct addrinfo hints, *servinfo, *p;
    int rv;
    char s[INET_ADDRSTRLEN];

    if (argc != 2)
    {
        fprintf(stderr, "usage: client hostname\n");
        exit(1);
    }

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
    {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    for (p = servinfo; p != NULL; p = p->ai_next)
    {
        if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
        {
            perror("client: socket");
            continue;
    }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
        {
            close(sockfd);
            perror("client: connect");
            continue;
        }
        break;
    }

    if (p == NULL)
    {
        fprintf(stderr, "client: failed to connect\n");
        return 2;
    }

    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof(s));

    printf("client: connecting to %s\n", s);

    freeaddrinfo(servinfo);

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE - 1, 0)) == -1)
    {
        perror("recv");
        exit(1);
    }

    buf[numbytes] = '\0';
    printf("client: received '%s'\n", buf);

    close(sockfd);
    return 0;
}
  • 编译运行
bash 复制代码
# 编译
gcc -o client01_demo client01.c

# 运行 - 在同一台终端试验
./client01_demo 127.0.0.1

参考

1 https://info.support.huawei.com/info-finder/encyclopedia/zh/IPv4.html

2 https://info.support.huawei.com/info-finder/encyclopedia/zh/IPv6.html

3 https://beej-zhcn.netdpi.net/