Linux——socket网络编程

一、主机字节序列和网络字节序列

(一)主机字节序

CPU在内存中进行数据存取的顺序。

二进制存在高位和低位之分:左边是高位,右边是低位;

内存存储存在高地址和低地址之分:一块空间起始处总是低地址(一个存储单元大于1字节的数据,就会存在字节序的问题)

  • 大端字节序:一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处(高地址存低位,低地址存高位,例如:0x01020304 -> 01 02 03 04)

  • 小端字节序:一个整数的高位字节存储在内存的高地址处,低位字节存储在内存的低地址处(高地址存高位,低地址存低位,例如:0x01020304 -> 04 03 02 01)

字节序的决定性:CPU架构

  • X86:小端字节序

  • MIPS:大端字节序

(二)网络字节序

在两台使用不同字节序的主机之间传递数据时,可能会出现冲突,所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序称为网络字节序。对方接收到数据后,可以根据自己的字节序进行转换

(三)字节序转换接口

字节序转换针对的数据类型:short,int,long,float,double...(大于1字节的类型)

(1)从主机字节序转换为网络字节序

  • 2字节数据的转换:uint16_t htons(uint16_t val);(常用于转换端口)
  • 4字节数据的转换:uint32_t htonl(uint32_t val);(常用于转换IP地址)

注意:短整型数据的转换不能使用 htonl

(2)从网络字节序转换为主机字节序

  • 2字节数据的转换:uint16_t ntohs(uint16_t val);(常用于转换端口)
  • 4字节数据的转换:uint32_t ntohl(uint32_t val);(常用于转换IP地址)

(3)点分十进制的转换

IP地址本质就是一个4字节的无符号整数,用ntohl,htonl进行转换即可;但是,为了使IP地址能够更加便于记忆,会采用点分十进制字符串进行表示(例如:192.168.12.12);所以很多时候,IP地址的转换,并不是转换实际的数字,而是一个字符串。

  • 将字符串点分十进制IPv4地址,转换为网络字节序数字IPv4地址
cpp 复制代码
struct in_addr inet_addr(const char *ipstr);
  • 将网络字节序数字IPv4地址,转换为字符串点分十进制IPv4地址
cpp 复制代码
const char* inet_ntoa(sruct in_addr addr); // 非线程安全接口
  • 将网络字节序整数IP地址转换为字符串IP地址(可转换IPv4或IPv6)
cpp 复制代码
const char* inet_ntop(int domain, void *src, char *dst, size_t size);

/*
domain:描述要转换的地址类型,AF_INET(ipv4)或 AF_INET6(ipv6)
src:网络字节序的数据地址
dst:转换后的字符串存放空间地址
size:dst空间的大小
*/
  • 将字符串的IP地址,转换为网络字节序IP地址(可转换为IPv4或IPv6)
cpp 复制代码
int inet_pton(int domain, const char *src, void *dst);

二、套接字编程

(一)网络通信

网络通信也是进程间通信方式的一种,相较于以前的进程间通信,网络通信可以实现跨主机通信

网络通信也是进程间通信方式,但是通信两端有不同的角色:

  • 客户端:主动向服务器发送请求,获取服务的一端,通常部署在客户机上
  • 服务端:通常部署在网络服务提供商的服务器上,接收客户端请求并提供服务

通信分为两种不同的架构:

  • C/S:客户端服务器模式(client/server)
  • B/S:浏览器服务器模式(browser/server)

(二)套接字地址结构

套接字:一套网络通信API接口

1.通用 socket 地址结构

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr

cpp 复制代码
struct sockaddr
{
    sa_family_t sa_family;
    char sa_data[14];
};

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应,常见的协议族和对应的地址族如下:

2.专用 socket 地址结构

TCP/IP协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于IPv4和IPv6

cpp 复制代码
struct in_addr
{
    u_int32_t s_addr;
};

struct sockaddr_in
{
    sa_family_t sin_family;  // 地址族 AF_INET
    u_int16_t sin_port;      // 端口号 用网络字节序表示
    struct in_addr sin_addr; // IVP4地址结构 s_addr以网络字节序表示IPV4地址
};

struct in6_addr
{
    unsigned char sa_addr[16]; //IPV6地址 用网络字节序表示
};

struct sockaddr_in6
{
    sa_family_t sin6_family;   // 地址族 AF_INET
    u_inet16_t sin6_port;      // 端口号 用网络字节序表示
    u_int32_t sin6_flowinfo;   // 流信息 应设置为0
    struct in6_addr sin6_addr; // IVP6地址结构 
    u_int32_t sin6_scope_id;   // scope ID
};

三、网络编程接口

(一)创建套接字

在内核中创建网络通信描述信息,并获取套接字的文件描述符

cpp 复制代码
int socket (int domain, int type, int protocol);

/*
domain:地址域,描述通信地址类型
    AF_UNIX:域间通信(本地通信),struct sockaddr_un
    AF_INET:IPV4通信,struct sockaddr_in
    AF_INET6:IPV6通信,struct sockaddr_in6

type:设置套接字的服务类型
    SOCK_STREAM:流式套接字,有序的,可靠的,双向的,基于字节流
    SOCK_DGRAM:数据报套接字,无连接的,不可靠的,有最大长度限制的
    SOCK_RAW:提供更底层原始的数据包捕获

protocol:协议类型,选择要使用的协议
    默认值 0:SOCK_STREAM是tcp协议;SOCK_DGRAM是udp协议
    IPPROTO_TCP:TCP协议
    IPPROTO_UDP:UDP协议

返回值:
    成功,则返回套接字的文件描述符(非负整数);
    失败,返回 -1
*/

(二)为套接字绑定地址信息

将sockfd与一个socket地址绑定

作用:告诉网卡,发送数据的时候,数据的源端地址信息是什么(当前设备网卡的IP地址+端口号);告知操作系统,网卡接收数据的时候,哪些数据,应该交给当前套接字进行处理

cpp 复制代码
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

/*
sockfd:套接字描述符,socket返回的描述符

addr:要绑定的地址信息

addrlen:地址信息长度

返回值:成功返回 0;失败返回 -1
*/

(三)连接服务器(用于TCP协议客户端)

客户端通过此系统调用来主动与服务器建立连接

cpp 复制代码
int connect(int sockfd,const struct sockaddr *serv_addr, socklen_t addrlen);

/*
serv_addr:要连接的服务端地址信息

addrlen:指定这个地址的长度

返回值:成功返回 0;失败返回 -1
*/

(四)开始监听(用于TCP协议服务端)

创建一个监听队列以存储待处理的客户连接

cpp 复制代码
int listen(int sockfd,int backlog);

/*
sockfd:被监听的socket套接字

backlog:同一时间能够接受最大客户端连接请求数量

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

(五)获取就绪连接(用于TCP协议服务端)

从listen监听队列中接收一个连接,返回一个新的连接socket,该socket唯一地标识了被接收地这个连接

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

/*
sockfd:执行过listen系统调用的监听socket

addr:获取呗接受连接的远端socket地址

addrlen:既要设置要获取多长的地址,并且还要返回实际的地址信息长度

返回值:成功则返回指定客户端通信句柄描述符;失败返回 -1
*/

(六)接收数据

接收数据的接口是一个阻塞接口(若没有数据则等待)

1.TCP的接收接口

读取sockfd上的数据

cpp 复制代码
ssize_t recv(int sockfd, void *buff, size_t len, int flags)

/*
sockfd:对于客户端来说就是socket的返回值;对于服务端来说是accept的返回值

buff:指定读缓冲区的位置

len:指定读缓冲区的大小

flags:为数据接收提供了额外的控制
    通常默认为 0,表示阻塞接收
    MSG_DONTWAIT:本次recv能拿到数据则直接返回,没有数据则报错返回
    MSG_PEEK:接收数据,但是不从缓冲区删除数据,常用于检测性接收

返回值:成功则返回实际读取到的数据长度;出错则返回-1;TCP通信连接断开,返回0
*/

2.UDP的接收接口

读取sockfd上的数据

cpp 复制代码
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

/*
buff:指定读缓冲区的位置

len:指定读缓冲区的大小

src_addr:记录发送端的socket地址

addrlen:指定该地址的长度

返回值:成功则返回实际读取到的数据长度;出错则返回-1
*/

(七)发送数据

发送数据的接口是一个阻塞接口(发送过快,导致数据将socket发送缓冲区塞满了就会阻塞)

1.TCP的发送接口

往socket上写入数据

cpp 复制代码
ssize_t send(int sockfd, const void *buff, size_t len, int flags);

/*
buff:指定写缓冲区的位置

len:指定写缓冲区的数据长度

flags:为数据发送提供了额外的控制
    通常默认为0,表示缓冲区满的时候,函数阻塞,等待缓冲区腾出空间

返回值:实际发送的数据长度; 出错则返回-1;
*/

2.UDP的发送接口

往socket上写入数据

cpp 复制代码
ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);

/*
buff:指定写缓冲区的位置

len:指定写缓冲区的数据长度

dest_addr:指定接收数据端的socket地址

addrlen:指定该地址的长度

返回值:实际发送的数据长度; 出错则返回-1
*/

(八)关闭套接字

关闭一个连接,实际上就是关闭该连接对应的socket

cpp 复制代码
  int close(int sockfd);

四、UDP编程流程

UDP提供的是无连接、不可靠的、数据报服务

(一)编程流程

  1. sokcet()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发;创建套接字时要指定使用的服务类型,使用udp协议选择数据报服务SOKC_DGRAM

  2. bind()方法是用来指定套接字使用的IP地址和端口


    IP地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址"127.0.0.1"。端口是一个16位的整形值,一般0-1024为知名端口,这类端口一般用户不能随便使用;1024-4096为保留端口,用户一般也不使用;4096以上为临时端口,用户可以使用;在Linux上,1024以内的端口号,只有root用户可以使用

  3. sendto()用来发送数据,由于UDP是无连接的,每次发送数据都需要指定对端的地址(IP和端口)

  4. recvfrom()接收数据,每次都需要传给该方法一个地址结构来存放发送端的地址;recvfrom()可以接收所有客户端发送给当前应用程序的数据,并不是只能接收某一个客户端的数据

  5. close()关闭连接

(二)代码编写

1.服务端

cpp 复制代码
/*
    编写一个简单的UDP通信服务端程序:
        功能:模拟简单的聊天程序,
            1.接收客户端消息进行展示
            2.从标准输入获取一个信息响应给客户端
*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api接口
#include <netinet/in.h> // 地址结构和协议宏声明
#include <arpa/inet.h>  // 字节序转换的api接口

int main()
{
    // 1.创建套接字
    // int socket(int domain, int type, int protocol);
    // AF_INET:IPv4通信;SOCK_DGRAM:数据报套接字
    // 协议类型也可以给0表示UDP,因为SOCK_DGRAM数据报套接字默认协议就是UDP
    int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sockfd < 0)
    {
        perror("创建套接字失败:");
        return -1;
    }

    // 1.5 构造地址信息结构,填充要绑定的地址信息
    //     绑定的IP地址必须是当前一有的网卡IP地址,绑定哪个网卡则接收的就是哪个网卡收到的数据
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9090); // 要转换为网络字节序
    // in_addr_t inet_addr(const char *__cp)
    // int inet_pton(int af, const char *src, void *dst);
    // 方法一
    // addr.sin_addr.s_addr = inet_addr("192.168.182.128");
    // 方法二
    inet_pton(AF_INET, "192.168.182.128", &addr.sin_addr);

    // 2.为套接字绑定地址信息
    // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int ret = bind(sockfd, (struct sockaddr *)&addr, addrlen);
    if (ret < 0)
    {
        perror("绑定地址信息失败");
        return -1;
    }

    while (1)
    {
        // 3.接受客户端数据
        // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
        struct sockaddr_in peerAddr;
        socklen_t addrlen = sizeof(struct sockaddr_in);
        char buffer[1024] = {0}; // 用于存放获取出来的数据
        ret = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&peerAddr, &addrlen);
        if (ret < 0)
        {
            perror("recvfrom接收数据失败");
            continue;
        }
        printf("客户端[%s:%d] say:%s\n", inet_ntoa(peerAddr.sin_addr), ntohs(peerAddr.sin_port), buffer);

        // 4.捕获键盘输入
        memset(buffer, 0x00, 1024);
        printf("服务器 say:");
        fflush(stdout);
        scanf("%s", buffer);

        // 5.向客户端响应数据
        // ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
        ret = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peerAddr, addrlen);
        if (ret < 0)
        {
            perror("sendto 发送数据失败");
            continue;
        }
    }

    // 6.关闭套接字
    close(sockfd);

    return 0;
}

2.客户端

cpp 复制代码
/*
    编写一个简单的UDP通信客户端程序:
        功能:模拟一个简单的聊天功能
            1.从标准输入捕获一个字符串,发送给服务端
            2.等待服务端的响应,接收后展示出来
*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api接口
#include <netinet/in.h> // 地址结构和协议宏声明
#include <arpa/inet.h>  // 字节序转换的api接口

#define CHECH_RESULT(ret, msg) \
    if ((ret) < 0)             \
    {                          \
        perror(msg);           \
        return -1;             \
    }

int main()
{
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    CHECH_RESULT(sockfd, "创建套接字失败");

    // 2.为套接字绑定地址信息(大多时候不需要主动绑定)

    // 2.5 构造地址信息结构
    struct sockaddr_in srvAddr;
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_port = htons(9090);
    srvAddr.sin_addr.s_addr = inet_addr("192.168.182.128");
    socklen_t addrlen = sizeof(struct sockaddr_in);

    while (1)
    {
        // 3.捕获键盘输入
        char buffer[1024] = {0};
        printf("客户端 say:");
        fflush(stdout);
        scanf("%s", buffer);

        // 4.发送数据
        ssize_t ret = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&srvAddr, addrlen);
        CHECH_RESULT(ret, "发送数据失败");

        // 5.接收服务端响应,进行展示
        memset(buffer, 0x00, 1024);
        ret = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&srvAddr, &addrlen);
        CHECH_RESULT(ret, "接收响应失败")
        printf("服务端 say:%s\n", buffer);
    }

    // 6.关闭套接字
    close(sockfd);

    return 0;
}

五、TCP编程流程

(一)编写流程

TCP提供的是面向连接的、可靠的、字节流服务

  • 面向连接:两端必须先建立连接,才能通信
  • 可靠传输:确保数据能够安全、有序的到达对端
  • 面向字节流:以字节为单位进行传输,不限制传输大小,先发先到

在TCP通信中,套接字是区分了两种:

  • 监听套接字:是最早服务端通过socket创建的套接字,只用于处理新连接
  • 通信套接字:是客户端连接到服务器成功后创建的新的套接字,用于与客户端通信
  1. sokcet()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发;创建套接字时要指定使用的服务类型,使用TCP协议选择流式服务SOCK_STREAM
  2. bind()方法是用来指定套接字使用的IP地址和端口
  3. connect()方法一般由客户端程序执行,用来与客户端建立连接
  4. listen方法时用来创建监听队列
  5. accept()用来从listen监听队列中接收一个连接,返回一个新的连接socket,该socket唯一地标识了被接收地这个连接
  6. send()方法用来向TCP连接的对端发送数据
  7. recv()方法用来接收TCP连接的对端发送来的数据
  8. close()关闭连接

(二)代码编写

1.服务端

cpp 复制代码
/*
    实现一个简单的tcp服务端程序:模拟简单聊天
        1.接收客户端的消息,进行展示
        2.从键盘捕获输入,发送给客户端
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h>  // 字节序转换htons

#define CHECK_RESULT(ret, msg) \
    if ((ret) < 0)             \
    {                          \
        perror(msg);           \
        return -1;             \
    }

int main()
{
    // 1.创建套接字
    // int socket(int domain, int type, int protocol);
    int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    CHECK_RESULT(listen_fd, "创建套接字失败");

    // 2.构造监听地址结构对象,为套接字绑定地址信息
    // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = inet_addr("192.168.182.128");
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
    CHECK_RESULT(ret, "绑定地址失败");

    // 3.开始监听
    // int listen(int listen_fd, int backlog);
    ret = listen(listen_fd, 1024);
    CHECK_RESULT(ret, "监听失败");

    while (1)
    {
        // 4.获取新连接(与客户端的通信都是通过获取的新连接进行的)
        // int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen);
        // insten_fd:监听套接字描述符
        // *addr:客户端地址信息
        // *addrlen:指定想要获取的地址长度,以及返回实际获取的地址长度
        // 返回值:新建连接的套接字描述符(用于往后的数据通信)
        struct sockaddr_in peerAddr;
        int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
        if (newfd < 0)
        {
            perror("获取新连接出错");
            continue;
        }
        char peer_ip[24] = {0};
        inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
        printf("新的客户端连接建立成功:[%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));
        while (1)
        {
            // 5.接收客户端消息,进行展示
            // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
            // 返回值:成功则返回世纪接收的数据长度
            //        失败出错,则返回-1
            //        若返回0,则表示连接断开

            char buffer[1024] = {0};
            ret = recv(newfd, buffer, 1023, 0);
            if (ret < 0)
            {
                perror("接收数据出错");
                continue;
            }
            else if (ret == 0)
            {
                printf("与客户端连接断开\n");
                close(newfd);
                break;
            }
            printf("客户端say:%s\n", buffer);

            // 6.捕获键盘输入,向客户端发送数据
            // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
            memset(buffer, 0x00, 1024);
            printf("输入数据:");
            fflush(stdout);
            scanf("%s", buffer);
            ret = send(newfd, buffer, strlen(buffer), 0);
            if (ret != strlen(buffer))
            {
                if (ret < 0)
                {
                    perror("发送数据出错");
                    close(newfd);
                    continue;
                }
                printf("数据发送不完整");
            }
        }
    }

    // 7.关闭套接字
    close(listen_fd);

    return 0;
}

2.客户端

cpp 复制代码
/*
    实现一个简单的tcp服务端程序:模拟简单聊天
        1.从键盘捕获数据,发送给服务器
        2.等待服务器的响应,进行展示
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h>  // 字节序转换htons

#define CHECK_RESULT(ret, msg) \
    if ((ret) < 0)             \
    {                          \
        perror(msg);           \
        return -1;             \
    }

int main()
{
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    CHECK_RESULT(sockfd, "创建套接字失败");

    // 2.绑定地址信息(不主动)
    // 3.向服务器发送连接请求
    // int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = inet_addr("192.168.182.128");
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int ret = connect(sockfd, (struct sockaddr *)&addr, addrlen);
    CHECK_RESULT(ret, "连接服务器失败");

    while (1)
    {
        // 4.捕获键盘数据,向服务器发送数据
        // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
        char buffer[1024] = {0};
        memset(buffer, 0x00, 1024);
        printf("输入数据:");
        fflush(stdout);
        scanf("%s", buffer);
        ret = send(sockfd, buffer, strlen(buffer), 0);
        if (ret < 0)
        {
            perror("发送数据失败");
            close(sockfd);
            return -1;
        }

        // 5.等待服务端响应,展示响应数据
        // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
        memset(buffer, 0x00, 1024);
        ret = recv(sockfd, buffer, 1023, 0);
        if (ret <= 0)
        {
            perror("接收数据失败");
            close(sockfd);
            return -1;
        }
        printf("服务端 say:%s\n", buffer);
    }

    // 6.关闭套接字
    close(sockfd);

    return 0;
}

六、多进程、多线程处理并发

(一)并发原理

tcp服务端存在的问题:

在socket套接字API中,accept获取新连接,recv接收数据,send发送数据,都是默认阻塞接口(功能未完成就会一直等待);这些阻塞接口导致,当前在单执行流中,无法正常的与每个客户端持续进行通信(要么只能与一个客户端持续通信;要么就只能与一个客户端通信一次)

解决方案:将每一个阻塞操作都放到一个单独的执行流中

多执行流有两种实现方案:

  • 方案一:多进程:更加稳定,健壮性更强
  • 方案二:多线程:成本更低,线程间通信更加方便

实现一个并发服务器(可以同时与多个客户端进行通信的服务器):服务端接受一个客户端的连接后,创建一个线程或者进程,然后在新创建的线程或进程中循环处理数据;主线程(父进程)只负责监听客户端的连接,并使用 accept() 接受连接,不进行数据的处理。

多执行流版本的并发服务器,并不是一个好的解决方案:资源有限

  1. 需要为每一个客户端连接都创建一个执行流

  2. 执行流并不是越多越好(多了就会增加cou时间片切换成本)

(二)代码编写

1.多进程(服务端)

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h>  // 字节序转换htons
#include <signal.h>

#define CHECK_RESULT(ret, msg) \
    if ((ret) < 0)             \
    {                          \
        perror(msg);           \
        return -1;             \
    }


// 实现一个多进程版本的服务器:创建进程,在子进程中与客户端进行通信
void worker(int sockfd)
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("创建子进程失败");
        return;
    }
    if (pid > 0)
    {
        return; // 父进程直接返回,回到main
    }
    while (1)
    {
        char buffer[1024] = {0};
        int ret = recv(sockfd, buffer, 1023, 0);
        if (ret < 0)
        {
            perror("接收数据出错");
            continue;
        }
        else if (ret == 0)
        {
            printf("与客户端连接断开\n");
            break;
        }
        printf("客户端say: %s\n", buffer);

        // 6.捕获键盘输入,向客户端发送数据
        // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
        memset(buffer, 0x00, 1024);
        printf("输入数据:");
        fflush(stdout);
        scanf("%s", buffer);
        ret = send(sockfd, buffer, strlen(buffer), 0);
        if (ret != strlen(buffer))
        {
            if (ret < 0)
            {
                perror("发送数据出错");
                close(sockfd);
                continue;
            }
            printf("数据发送不完整\n");
        }
    }
    close(sockfd);
    exit(0); // 当前在子进程中,这里退出的是子进程
}

int main()
{
    //主动忽略SIGCHLD信号:告诉系统我不关注子进程状态
    //显式的告诉系统,子进程退出后,直接释放所有资源
    signal(SIGCHLD, SIG_IGN);

    // 1.创建套接字
    // int socket(int domain, int type, int protocol); 
    int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    CHECK_RESULT(listen_fd, "创建套接字失败");

    // 2.构造监听地址结构对象,为套接字绑定地址信息
    // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = inet_addr("192.168.182.128");
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
    CHECK_RESULT(ret, "绑定地址失败");

    // 3. 开始监听
    ret = listen(listen_fd, 1024);
    CHECK_RESULT(ret, "监听失败");

    while (1)
    {
        struct sockaddr_in peerAddr;
        int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
        if (newfd < 0)
        {
            perror("获取新链接出错");
            continue;
        }
        char peer_ip[24] = {0};
        inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
        printf("新的客户端连接建立成功: [%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));

        worker(newfd);
        close(newfd); //父进程中并没有用到这个描述符
    }
    return 0;
}

多进程注意事项:

  1. 通过信号处理,避免产生僵尸进程

  2. 父进程中创建子进程之后,要关闭新建套接字(父进程没用到)

2.多线程(服务端)

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>     // close
#include <sys/socket.h> // socket api
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h>  // 字节序转换htons
#include <pthread.h>

#define CHECK_RESULT(ret, msg) \
    if ((ret) < 0)             \
    {                          \
        perror(msg);           \
        return -1;             \
    }

void *entry(void *arg)
{
    int sockfd = (int)arg;
    while (1)
    {
        // 5.接收客户端消息,进行展示
        // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
        // 返回值:成功则返回世纪接收的数据长度
        //        失败出错,则返回-1
        //        若返回0,则表示连接断开

        char buffer[1024] = {0};
        int ret = recv(sockfd, buffer, 1023, 0);
        if (ret < 0)
        {
            perror("接收数据出错");
            continue;
        }
        else if (ret == 0)
        {
            printf("与客户端连接断开\n");
            break;
        }
        printf("客户端say:%s\n", buffer);

        // 6.捕获键盘输入,向客户端发送数据
        // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
        memset(buffer, 0x00, 1024);
        printf("输入数据:");
        fflush(stdout);
        scanf("%s", buffer);
        ret = send(sockfd, buffer, strlen(buffer), 0);
        if (ret != strlen(buffer))
        {
            if (ret < 0)
            {
                perror("发送数据出错");
                close(sockfd);
                continue;
            }
            printf("数据发送不完整");
        }
    }
    close(sockfd);
}

// 创建一个线程,负责通过当前的描述符与客户端进行通信
void worker(int sockfd)
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, entry, (void *)sockfd);
    if (ret != 0)
    {
        perror("创建工作线程失败");
        return;
    }
    pthread_detach(tid); // 分离线程,让线程退出后自动释放所有资源,不需要被等待
}

int main()
{
    // 主动忽略SIGCHLD信号------告诉系统我不关注子进程状态
    // 显示的告诉系统,子进程退出后,直接释放所有资源
    signal(SIGCHLD, SIG_IGN);

    // 1.创建套接字
    // int socket(int domain, int type, int protocol);
    int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    CHECK_RESULT(listen_fd, "创建套接字失败");

    // 2.构造监听地址结构对象,为套接字绑定地址信息
    // int bind(int listen_fd, const struct sockaddr *addr, socklen_t addrlen);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = inet_addr("192.168.182.128");
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int ret = bind(listen_fd, (struct sockaddr *)&addr, addrlen);
    CHECK_RESULT(ret, "绑定地址失败");

    // 3.开始监听
    // int listen(int listen_fd, int backlog);
    ret = listen(listen_fd, 1024);
    CHECK_RESULT(ret, "监听失败");

    while (1)
    {
        // 4.获取新连接(与客户端的通信都是通过获取的新连接进行的)
        // int accept(int listen_fd, struct sockaddr *addr, socklen_t *addrlen);
        // insten_fd:监听套接字描述符
        // *addr:客户端地址信息
        // *addrlen:指定想要获取的地址长度,以及返回实际获取的地址长度
        // 返回值:新建连接的套接字描述符(用于往后的数据通信)
        struct sockaddr_in peerAddr;
        int newfd = accept(listen_fd, (struct sockaddr *)&peerAddr, &addrlen);
        if (newfd < 0)
        {
            perror("获取新连接出错");
            continue;
        }
        char peer_ip[24] = {0};
        inet_ntop(AF_INET, &peerAddr.sin_addr, peer_ip, 23);
        printf("新的客户端连接建立成功:[%s:%d]\n", peer_ip, ntohs(peerAddr.sin_port));
        worker(newfd);
        // close(newfd); // 线程间资源共享,这里如果关闭描述符会导致普通线程无法使用
    }

    // 7.关闭套接字
    close(listen_fd);

    return 0;
}

多线程注意事项:

  1. 创建线程后,要分离线程(不需要被等待了)

  2. 主线程虽然不用newfd,但是也不能关闭(线程间共享进程资源)

相关推荐
道尔柯南1 小时前
【Linux】网络基础概念
linux·运维·网络
小宋10211 小时前
MCP 工具也有供应链风险:注册中心、签名校验、版本锁定与白名单
开发语言·网络·人工智能·php
zxanz11 小时前
SSL 证书会影响 GEO 跟 SEO 排名吗?
网络·网络协议·ssl
傲世仙尊1 小时前
从抢票事故到锁的原理-互斥条件变量与生产消费模型
linux·驱动开发
AI+程序员在路上1 小时前
AP6275S蓝牙双接口解析:HCI UART与PCM的分工与协同
linux·c语言·物联网
小此方2 小时前
Linux网络(十八):TCP连接管理详解:从三次握手、四次挥手到CLOSE_WAIT与TIME_WAIT,深入理解2MSL与端口复用
linux·网络·网络协议·tcp/ip
进击的荆棘2 小时前
Linux系统——文件(上)
linux·运维·服务器·文件
k4m7v2pz2 小时前
ThinkPad E490 风扇 “转两秒就停“ 实录:迟滞曲线 + 最短运转时间
linux·脚本·archlinux·thinkpad·风扇·散热
拂拉氏2 小时前
【知识讲解】 Linux文件系统底层认识
linux·文件系统