Linux网络 - Socket编程(IPv4&IPv6)

大家学习前可以稍微看看网络基础博客

我们重温一下UDP和TCP的性质

我们主要是以IPv4来讲解,如果遇到IPv6的额外操作我会穿插进去。并在结尾总结IPv6的整个实现过程和怎么保证IPv6兼容IPv4

UDP/TCP性质

TCP类似电话,UDP类似快递发信封。我们假定发信封的速度被加速到和电话一样

UDP:是没有连接的,不存在像电话一样一直连着接着,而是类似于写信,我发你一份,++掉了就掉了++ ,可能对方也不知道你给我发了个东西,而且我发的信封几个可能++乱序到达++ ,因为车有堵车的有没有堵车的。但是++发的很高效++ ,我可以不受约束,随意发送。我信封++可以一下发送一大坨纸,一本书都可以,一次性把要说的全发完++ 。++发完我就完事了,传递过程很短++ 。所以具有无连接,不可靠,面向数据报,一次性发送整个报文的特性。

TCP:是有连接的,它会一直处于通话中,但是我们正式发送数据前,我们会确认是否真的建立连接了,++例如我说:"你那边听得到吗?" 你也会说:"我听得到,你那边听得到吗?" ,我会回答:"我也听得到++ " 。那么这个操作就保证建立连接 了,但是建立速度没有信封快,因为信封根本不需要连接。++如果对方没听到就会给你说前面有句话没听到,那么你要重新说,这保证了每句话都能听到++ ,具有可靠性 。可能电话信号不好要说慢点,可能++对方一下理解不完只能一句一句的分开说,一次性不会把话说完++ 。所以具有它有连接、可靠、面向字节流的特性。

那么大家应该也知道面向报文是什么意思了,就是一次性发完整个报文。面向字节流可能不会一下全发,可能会分成一段一段的字节流

UDP/TCP相关接口

我们最开始讲的socket和bind是TCP和UDP共用的

socket

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain 是网络家族类型的声明,一般使用AF_INET(IPv4) AF_INET6(IPv6)

type 主要是给操作系统存储传输层用什么协议,一般使用SOCK_DGRAM(Datagram数据报 )表示UDP协议 ,使用SOCK_STREAM(Stream字节流)表示TCP协议。

protocol 显示选择什么类型,这里默认0,在前面两个选项选择下已经可以确定了。

所以对于UDP来说就是:(IPv4)

cpp 复制代码
int fd = socket(AF_INET,SOCK_DGREAM,0);

对于TCP就是

cpp 复制代码
int fd = socket(AF_INET,SOCK_STREAM,0)

IPv6就是将AF_INET换成AF_INET6

Linux下一切皆文件,那么socket和open一样,底层也会创建一种特殊的套接字文件,它不仅具有文件性质,还保存了网络家族类型,协议类型。

这里我们结合TCP一起说一下,如果是UDP ,那么就会直接用这个返回的文件描述符来作为收发报文 。但是TCP 下这个只是监听套接字,它只是用于监听是否有连接,后续进行报文的收发需要使用TCP监听返回的套接字来操作。

所以到这一步,我们只是在本地创建了一个文件,只是为网络连接打下基础,并没有与外界做出什么连接通信。

网络字节序转换函数

在正式了解bind函数之前,我们先要知道网络字节序怎么转换

端口转换

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

uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);

host 变 net 的短整型,也就是port。这里帮助记忆,函数用法我就不多解释了

IP转换

这里我直接推荐这个终极函数,其他的转换都不要,这个可以操作IPv4和IPv6的

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

int inet_pton(int af, const char *src, void *dst);

af:地址组IF_INET /IF_INET6

src:要转换的ip 例如"163.0.0.1"

dst:转换后的二进制序列,这个是大端转换好的二进制

返回值:成功返回0

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

const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

af:依旧地址族IF_INET /IF_INET6

src:要转换的大端二进制

dst:转换后的字符串 ,返回值也是返回的这个字符串,如果错误返回空

size:是dst的长度 IPv4长度是16 IPv6长度是46(包括必要的反斜杠零)

bind

我们知道socket = ip +port ,但是上面库库一顿操作,也没见ip和port啊。那么就需要bind来绑定了。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个sockfd就是上面我们创建的sock文件描述符,这个不难理解。

第二个是一个结构体,sockaddr_in就是网络的子结构体,第二个是本地通信用的不管

cpp 复制代码
struct sockaddr {
    sa_family_t sa_family;  // 地址族(与 sockaddr_in 的 sin_family 一致)
    char        sa_data[14];// 存储地址+端口的原始数据(14 字节)
};

上面是父类的,填充了14字节,下面出现sin_zero是填充到和父类一样大的。

所以我们传一个sockaddr_in。那么我们来看看sockaddr_in的具体结构,上面也有:

cpp 复制代码
#include <netinet/in.h>

// IPv4 套接字地址结构体
struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族(Address Family)
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IPv4 地址(网络字节序)
    char           sin_zero[8];  // 填充字段,使结构体大小与 sockaddr 一致
};
// 嵌套的 IPv4 地址结构体(sin_addr 的类型)
struct in_addr {
    in_addr_t       s_addr;      // 32位 IPv4 地址(网络字节序)
};

这里sin_family只能填AF_INET

sim_port 传的是大端的,记得用htons转换

sim_addr记得用inet_pton转换

sin_zero无实际意义,因此在初始化sockaddr_in时先用memset函数全置为0

我们还得了解一下IPv6的结构体sockaddr_in6:

cpp 复制代码
#include <netinet/in.h>

// IPv6 套接字地址结构体
struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族(固定为 AF_INET6)
    in_port_t       sin6_port;     // 端口号(网络字节序,16位)
    uint32_t        sin6_flowinfo; // 流信息(IPv6 流标签,低20位有效)
    struct in6_addr sin6_addr;     // IPv6 地址(128位,网络字节序)
    uint32_t        sin6_scope_id; // 作用域ID(用于链路本地地址等)
};
// 嵌套的 IPv6 地址结构体(sin6_addr 的类型)
struct in6_addr {
    uint8_t s6_addr[16]; // 128位 IPv6 地址,以16个字节(8位/字节)存储
};

第一个和第二个和sockaddr_in的是一模一样的,大小完全相同,直接操作就行。

sin6_flowinfo:直接设置为0即可

sin6_addr:使用int_pton即可

sin6_scope_id:设置为0即可

所以sockaddr_in6和sockaddr_in一样,先menset为0,然后设置家族、port、ip完事

客户端可以不使用bind绑定端口,服务器必须要绑定端口,因为客户端必须知道服务器端口才能通信。客户端时主动给服务端通信的,因此客户端的端口可以随机。

读缓冲区和写缓冲区

我们在学基础IO的时候,应该了解过文件读写都有缓冲区,这样可以避免平繁的系统调用。那么对于网络socket是否有呢?也是有的,通过其实现了网络通信的全双工

socket的读写发送报文,都要通过这个发送出去。

读缓冲区

对于UDP,因为一次性发送整个报文,因此在读缓冲区会隔离每个报文,这样上层读取时不会因为几个报文在一起,不知道咋读。所以一次上层读取就会给上层一个报文,如果没有报文就会默认阻塞

对于TCP,因为它发送可能会分包 ,这导致到读缓冲区的报文操作系统不知道那几个一起才是完整报文(黏包问题 )。因此操作系统不会隔离报文,让它们数据挨在一起,需要用户在上层自己分包。它对上层就是有数据就传给上层,没有默认阻塞

写缓冲区

对于UDP,它不需要什么额外操作,直接发就行了。只不过会遇到缓冲区慢的情况,如果满了,默认会等待阻塞

对于TCP,因为涉及到可靠性,因此发送不是按报文发的,而是多少数据发送更加稳妥。如果写入不下默认会阻塞

对于UDP和TCP具体的操作我们在后续博客具体讲解。

这是具体的通信图,可以看出每个

全局非阻塞设置

在接触其他接口之前,我们要回顾一下全局的非阻塞设置

如果要设置为非阻塞,则要用fcntl函数

cpp 复制代码
#include <fcntl.h>
int flags = fcntl(listen_fd, F_GETFL, 0);
fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);

// 此时 accept() 无连接时会立即返回 -1,errno = EAGAIN
int conn_fd = accept(listen_fd, ...);
if (conn_fd == -1) {
    if (errno == EAGAIN) {
        printf("暂无新连接\n");
    }
}

还可以使用多路复用select、poll、epoll,这里先不介绍

发送函数

首先我们梳理一下默认情况下的发送函数逻辑,对于UDP会一次性往发送整个数据报,如果一次性发送不了就会阻塞等待,直到能够一下发送完 。如果是TCP,如果一次性发送不了就会截断,尽可能塞满缓冲区,然后等待缓冲区有空闲位置,一直塞直到数据全部进入。

如果设置非阻塞后,如果是UDP,它这次发送不出去,塞不下就会直接返回。如果是TCP,它会把能写的写了,返回实际写入的数据,如果一个都写不进去就会返回错误

接收函数

我们先梳理默认情况下,udp和tcp都一样,如果没有数据就会阻塞。

如果设置非阻塞后,如果没有数据就会返回错误,因此我们要对接收情况的错误返回进行判断。

这个错误判断会设置在errno里面,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK(这两个是相同的)则是需要重新发送,并不是其他错误。下面我也会再提,加深印象

setsockopt

我们知道了socket会创建一个记录套接字相关信息的结构体。那么我们来看看具体套接字信息的创建。

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname
                      ,const void *optval, socklen_t optlen);

sockfd 要配置的fd,就是socket函数的返回值

level

可以选用一下四个:

optname

level = SOL_SOCKET:
cpp 复制代码
设置地址复用(避免端口占用问题)
int reuse_addr = 1;
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(reuse_addr)) < 0)     {
    perror("setsockopt SO_REUSEADDR failed");
    close(sock_fd);
    return -1;
}
cpp 复制代码
//设置接收缓冲区为1MB
int rcv_buf = 1024 * 1024; // 1MB
socklen_t optlen = sizeof(rcv_buf);
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_buf, optlen) == -1) {
    perror("setsockopt SO_RCVBUF failed");
    close(sockfd);
    return 1;
}
// 设置发送缓冲区为1MB
int snd_buf = 1024 * 1024;
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &snd_buf, sizeof(snd_buf)) == -1) {
    perror("setsockopt SO_SNDBUF failed");
    close(sockfd);
    return 1;
}
cpp 复制代码
// 设置接收超时为5秒(recvfrom()阻塞超过5秒会返回-1,errno=EAGAIN)
struct timeval recv_timeout;
recv_timeout.tv_sec = 5;  // 秒
recv_timeout.tv_usec = 0; // 微秒
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &recv_timeout, sizeof(recv_timeout)) == -1) {
    perror("setsockopt SO_RCVTIMEO failed");
    close(sockfd);
    return 1;
}

// 设置发送超时为3秒
struct timeval send_timeout = {3, 0};
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout)) == -1) {
    perror("setsockopt SO_SNDTIMEO failed");
    close(sockfd);
    return 1;
}
level = IPPROTO_TCP
cpp 复制代码
// 禁用Nagle算法(实时通信场景必备)
int nodelay = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)) == -1) {
    perror("setsockopt TCP_NODELAY failed");
    close(sockfd);
    return 1;
 }
level = IPPROTO_IPV6

使用一下函数后,可以使IPv6兼容IPv4

cpp 复制代码
int ipv6_only = 0;
if (setsockopt(sock_fd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6_only, sizeof(ipv6_only)) < 0) {
    perror("setsockopt IPV6_V6ONLY failed");
    close(sock_fd);
    return -1;
}

UDP 接口

下面的函数就和TCP接口的不同了,因此分开来说。不同的原因已经在socket函数就开始了。UDP的socket返回的fd是可以直接读写的入口了,而TCP的socket返回的fd只是监听窗口。

sendto

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

sockfd 就是socket返回的fd参数

buf 要发的数据

len 要发的数据大小

flags 一般设置为0即可

dest_addr 目的端的addr 传对方的dest_addr即可

addrlen 传dest_addr的长度

返回值:大于等于0时表示正确发送对应长度的报文。小于0的时候即返回-1,并不是表示必定有错!这时我们需要查看errno,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK,它表示在非阻塞情况下未能接收到数据。

recvfrom

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

sockfd 传socket的返回值

buf是接收的数据

len是buf的容量 ,因为UDP一次性读一个报文,如果len长度不够就会导致超出部分被丢弃

flags阻塞设为0 非阻塞设为MSG_DONTWAIT

dest_addr 存储发送方的addr

addrlen 传dest_addr的长度

返回值:大于等于0时表示正确接收对应长度的报文,udp是可以发送空包的。小于0的时候即返回-1,并不是表示必定有错!这时我们需要查看errno,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK,它表示在非阻塞情况下未能接收到数据。

connect

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

这个是给socket设置一个默认发送方的接口,这样就不用频繁使用sendto来发送了。这样不用频繁校验传的地址和端口是否合法,不用转换sockaddr_in,也不用查路由表确定出口。对于高频发送的场景,会带来10%-30%的速度优化。而这个可以用在客户端这种发送端不变的场景

我们绑定之后,就可以直接使用send和recv接口来发送和接收了,也减少了代码书写量。(send,recv在TCP讲解)

close

关闭fd,这个不用多说

TCP 接口

listen

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

sockfd是socket,即传的监听套接字fd。

backlog:连接请求队列的最大长度,通常设为5、10SOMAXVCNN

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

当调用这个函数之后,操作系统就会持续监听是否有连接到来并发在定义好长度的请求队列里面,等待后续操作取出

accept

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

向listen的监听队列里面取出连接,如果为空则会阻塞

返回值:非-1时表示正确接收对应长度的报文。小于0的时候即返回-1,并不是表示必定有错!这时我们需要查看errno,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK,它表示在非阻塞情况下未能接收到数据。

connect

cpp 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

对于客户端,不需要进行监听和绑定,但是需要进行连接,所以connect是给tcp客户端专用的。系统会自动给客户端一个临时的端口

send

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

sockfd是accept的连接套接字,buf、len不用多说了

flags是标志位,0默认。

如果要永久设置非阻塞,需要用到fcntl

cpp 复制代码
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

返回值:非-1时表示正确发送对应长度的报文。小于0的时候即返回-1,并不是表示必定有错!这时我们需要查看errno,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK,它表示在非阻塞情况下未能接收到数据。

recv

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags

sockfd是accept的连接套接字,buf、len不用多说了

flags:0 默认阻塞 MSG_WAITALL :等待收满指定长度的数据才返回。 MSG_OOB :发送/接收带外数据。 MSG_DONTWAIT: 非阻塞操作。

返回值:大于0时表示正确接收对应长度的报文。等于0的时候表示对端退出了。小于0的时候即返回-1,并不是表示必定有错!这时我们需要查看errno,如果是EINTER则是信号中断,如果是EAGAIN或者EWOULDBLOCK,它表示在非阻塞情况下未能接收到数据。

close/shutdown

cpp 复制代码
#include <unistd.h>
int close(int sockfd);

#include <sys/socket.h>
int shutdown(int sockfd, int how);

SHUT_RD(0): 关闭读端,不再接收。

SHUT_RT(1): 关闭写端(发送FIN),但可继续接收。用于优雅关闭

SHUT_RDRT (2): 双向关闭。UDP编程

因为只是简单的实现,也不涉及非阻塞,因此我对接收发送的返回值可能判断的比较鲁莽,实际应该判断好EAGAIN EINTER

服务端

那么就来实操一下,先实现一下服务端。

这里我们要知道,怎么给服务端绑定地址,默认是绑定0.0.0.0即可,这样会让服务器接收所有当下的网卡报文。

cpp 复制代码
#pragma once
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/file.h>
#include<fcntl.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
using namespace std;
class udpServer{
public:
    udpServer(uint16_t port):_port(port){
        _fd = socket(AF_INET,SOCK_DGRAM,0);
    }
    udpServer(const udpServer&)=delete;
    udpServer(udpServer&&)=delete;
    udpServer& operator=(const udpServer&)=delete;
    udpServer& operator=(udpServer&&)=delete;
    string recv(sockaddr_in&in){
        char buff[4096];
        socklen_t len = sizeof(in);
        ssize_t ret = recvfrom(_fd,buff,sizeof(buff)-1,0,(sockaddr*)&in,&len);
        if(ret==-1)throw std::runtime_error("接收失败");
        buff[ret]=0;
        cout<<"收到消息:"<<buff<<endl;
        return buff;
    }
    void send(const string&buf,const sockaddr_in&in){
        ssize_t ret =sendto(_fd,buf.c_str(),buf.size(),0,(const sockaddr*)&in,sizeof(in));
        if(ret==-1){
            throw std::runtime_error("发送失败");
        }
    }
    void run(){
        uint32_t ip;
        inet_pton(AF_INET,"0.0.0.0",&ip);
        uint16_t port = htons(_port);
        sockaddr_in in;
        memset(&in,0,sizeof(in));
        in.sin_addr.s_addr = ip;
        in.sin_family=AF_INET;
        in.sin_port = port;
        int ret = bind(_fd,(const sockaddr*)&in,sizeof(in));
        if(ret ==-1){
            cout<<"绑定失败"<<endl;
            exit(-1);
        }
        while(true){
            try{
                sockaddr_in netin;
                string ret = recv(netin);
                send(ret,netin);
            }catch(const std::runtime_error&x){
                cout<<x.what()<<endl;
            }

        }
    }

private:
    uint16_t _port;
    int _fd;
};

然后在main函数里面即可使用了

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include"udpserver.h"
using namespace std;



int main(int argv,char*argc[]){
    if(argv!=2){
        cout<<"should udp 8080"<<endl;
        return -1;
    }
    udpServer udp(stoi(argc[1]));
    udp.run();
    return 0;
}

客户端

客户端可以不使用bind绑定端口,服务器必须要绑定端口,因为客户端必须知道服务器端口才能通信。客户端时主动给服务端通信的,因此客户端的端口可以随机。

客户端可以使用connect优化,这里不用类封装了,直接写

cpp 复制代码
#include<iostream>
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/file.h>
#include<fcntl.h>
#include<string>
#include<netinet/in.h>
#include<cstring>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;

int main(int argv,char*argc[]){
    if(argv!=3){
        cout<<"should ./udpclient 127.0.0.1 8080"<<endl;
        return -1;
    }
    string strIp(argc[1]);
    uint16_t port = stoi(argc[2]);
    
    //转为网络字节序
    uint32_t netIP;
    inet_pton(AF_INET,strIp.c_str(),&netIP);
    uint16_t netPort = htons(port);

    int fd = socket(AF_INET,SOCK_DGRAM,0);
    sockaddr_in in;
    memset(&in,0,sizeof(in));
    in.sin_family=AF_INET;
    in.sin_port = netPort;
    in.sin_addr.s_addr =netIP;


    connect(fd,(const sockaddr*)&in,sizeof(in));
    
    string buffer = "你好!";

    while(true){
        //sendto(fd,buffer.c_str(),buffer.size(),0,(const sockaddr*)&in,sizeof(in));
        send(fd,buffer.c_str(),buffer.size(),0);
        char arr[4096];
        //ssize_t ret = recvfrom(fd,arr,sizeof(arr)-1,0,nullptr,nullptr);
        ssize_t ret = recv(fd,arr,sizeof(arr)-1,0);
        arr[ret]=0;
        cout<<arr<<endl;
        sleep(1);
    }
    return 0;
}

TCP编程

因为只是简单的实现,也不涉及非阻塞,因此我对接收发送的返回值可能判断的比较鲁莽,实际应该判断好EAGAIN EINTER

TCP编程要复杂的多,因为涉及到黏包问题,所以我们还得讲一下怎么处理黏包

黏包问题

对于黏包问题,最好解决的方式,就是在发送数据头部加上一个固定字节长度的数据,这样每次读取数据会先看数据长度,然后拿取对应长度的数据报做解析,这里我就不实现了。以免为大家增加理解负担。但是这个是很重要的东西。

服务端

cpp 复制代码
#pragma once
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/file.h>
#include<fcntl.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<vector>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
class tcpServer{
public:
    tcpServer(uint16_t port):_port(port){
        _fd = socket(AF_INET,SOCK_STREAM,0);
    }
    tcpServer(const tcpServer&)=delete;
    tcpServer(tcpServer&&)=delete;
    tcpServer& operator=(const tcpServer&)=delete;
    tcpServer& operator=(tcpServer&&)=delete;
    void run(){
        uint16_t port = htons(_port);
        uint32_t ip;
        inet_pton(AF_INET,"0.0.0.0",&ip);
        sockaddr_in in;
        memset(&in,0,sizeof(in));
        in.sin_port=port;
        in.sin_family=AF_INET;
        in.sin_addr.s_addr = ip;
        bind(_fd,(const sockaddr*)&in,sizeof(in));

        int listenRet = listen(_fd,SOMAXCONN);
        if(listenRet==-1)throw runtime_error("listen error");

        struct sigaction sg;
        memset(&sg,0,sizeof(sg));
        sg.sa_handler=SIG_IGN;
        
        sigaction(SIGCHLD,&sg,nullptr);//将子进程退出设置为不等待父进程回收

        while(true){
            sockaddr_in in;
            socklen_t len;
            int fd = accept(_fd,(sockaddr*)&in,&len);
            pid_t id = fork();
            if(id ==0){
                if(fork()>0)exit(0);//子进程退出,孙子进程来执行收发任务
                while(true){
                    char buffer[4096];
                    int ret =recv(fd,buffer,sizeof(buffer)-1,0);
                    if(ret==-1){
                        if(errno==EAGAIN||errno==EINTR)continue;
                        throw runtime_error("recv error");
                    }
                    else if(ret==0){
                        cout<<"子进程"<<getpid()<<"的对端关闭连接"<<endl;
                        exit(0);
                    }
                    buffer[ret]=0;
                    cout<<"子进程"<<getpid()<<"接收到:"<<buffer<<endl;
                    send(fd,buffer,ret,0);
                }
            }
        }
    }

private:
    uint16_t _port;
    int _fd;//监听套接字
    vector<int>_fds;
};
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include"udpserver.h"
using namespace std;
int main(int argv,char*argc[]){
    if(argv!=2){
        cout<<"should udp 8080"<<endl;
        return -1;
    }
    udpServer udp(stoi(argc[1]));
    udp.run();
    return 0;
}

我们可以使用nc -v ip port来测试一下

完全没问题。对这个指令不会的可以去看看我的上一个博客

客户端

cpp 复制代码
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/file.h>
#include<fcntl.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<vector>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
int main(int argv,char*argc[]){
    if(argv!=3){
        cout<<"should ./udpclient 127.0.0.1 8080"<<endl;
        return -1;
    }
    uint16_t port = htons(stoi(argc[2]));
    uint32_t ip ;
    inet_pton(AF_INET,argc[1],&ip);
    sockaddr_in in;
    memset(&in,0,sizeof(in));
    in.sin_addr.s_addr=ip;
    in.sin_family=AF_INET;
    in.sin_port=port;

    int fd = socket(AF_INET,SOCK_STREAM,0);
    connect(fd,(const sockaddr*)&in,sizeof(in));

    while(true){
        string str;
        getline(cin,str);
        send(fd,str.c_str(),str.size(),0);
        char buffer[4096];
        ssize_t ret = recv(fd,buffer,sizeof(buffer)-1,0);
        buffer[ret]=0;
        cout<<buffer<<endl;
    }

    return 0;
}

IPv6总结和兼容IPv4

IPv6的编程其实和IPv4的区别不大,第一个区别就是网络组的AF_INET6声明,只要是要传的就传AF_INET6,例如socket创建时,sockaddr_in6传参时,inet_pton的传参时,第二个就是sockaddr_in6,但是初始化也是memset->port->ip->AF_INET6,和sockaddr_in没多大区别。

基本上就可以保证IPv6编程时可行的了

兼容IPv4

只要在setsockopt的 level = IPPROTO_IPV6

cpp 复制代码
int ipv6_only = 0;
if (setsockopt(sock_fd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6_only, sizeof(ipv6_only)) < 0) {
    perror("setsockopt IPV6_V6ONLY failed");
    close(sock_fd);
    return -1;
}

即可实现向下兼容

相关推荐
噔噔君2 小时前
ip link show输出详解
网络·网络协议·tcp/ip
阿钱真强道2 小时前
07 jetlinks-ubuntu20-rk3588-部署
linux·运维·服务器·网络协议·tcp/ip
Realdagongzai2 小时前
Python学习过程记录3-操作列表
linux·vscode·python·kernel
CSDN_RTKLIB2 小时前
多线程锁基础
c++
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:搜索-BFS初识
c语言·数据结构·c++·算法·visual studio·宽度优先
燃于AC之乐2 小时前
【Linux系统编程】进程地址空间完全指南:页表、写时拷贝与虚拟内存管理
linux·操作系统·虚拟内存·进程地址空间
网硕互联的小客服2 小时前
站群服务器里的8C/4C/2C/1C有什么区别?选择哪个比较好?
运维·服务器·网络
T_Fire_of_Square2 小时前
crewai 知识库针对信息安全应急演练的定位和使用
网络·人工智能
_OP_CHEN2 小时前
【Linux系统编程】(二十三)从块到块组:Ext2 文件系统核心架构的初步认识
linux·操作系统·文件系统·c/c++·ext2文件系统·磁盘分区·块组