Linux网络套接字编程(socket详解)

前言

套接字编程其实就是网络编程,套接字实际就是一套网络通信程序编写的接口,通过这些接口,并且提供相关信息,就可以实现传输层以下几层的操作。

网络通信中涉及两台主机之间的通信:客户端(主动发送请求)、服务端(被动接收请求)。

一:TCP/UDP协议的基本认识

在TCP/IP网络体系结构中,TCP协议和UDP协议是传输层两种典型的协议,为上层用户提供级别的通信可靠性。

1.1 TCP:传输控制协议(Transport Control Protocol)

传输特点: 有连接、可靠传输、面向字节流

TCP通信需要建立连接(打电话),确保数据被对方收到,有序的安全的字节流传输服务

应用场景: 数据传输安全性要求高(文件传输)

1.2 UDP:用户数据报协议(User Data Protocol)

传输特点: 无连接、不可靠、面向数据报

UDP通信不需要建立连接(发短信),不确保数据是否被对方收到,无序的不可靠的数据块传输

应用场景: 数据传输实时性要求高(视频传输 )

二:UDP通信流程

在这里插入图片描述
注意:客户端用哪个源端地址信息发送数据不影响大局,只要服务端会回复到客户端绑定的源端地址信息就可以。(客户端一旦自己绑定源端地址信息,若选用的端口已经被占用则会绑定失败,所以不推荐客户端主动绑定源端地址信息)

2.1 UDP套接字相关接口
  1. 创建套接字

int socket(int domain, int type, int protocal) 返回一个非负整数(找到socket结构体)

domain:地址域

不同的协议版本有不同的地址结构——IP地址结构:IPv4(AF_INET)、IPv6(AF_INET6)

确定socket通信使用哪种协议版本的地址结构

type:套接字类型

数据报套接字: SOCK_DGRAM提供数据报传输服务(无连接、不可靠、有最大长度限制的消息传输服务)

流式套接字: SOCK_STREAM提供字节流传输服务 (有序、可靠、基于连接的消息传输服务)

protocal:协议类型

数据报套接字:默认UDP协议(IPPROTO_UDP)

流式套接字:默认TCP协议(IPPROTO_TCP)

  1. 为套接字绑定地址信息

int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)

sockfd:创建套接字返回的描述符

addr: 地址信息的结构(绑定各种各样的地址信息)

bind可以绑定不同的地址结构,为了实现接口统一,用户定义时定义自己需要的地址结构,绑定时统一将类型强转为sockaddr*
在这里插入图片描述

addrlen: 地址信息的长度

  1. 接收数据

int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len)

sockfd:指定内核中的socket结构体(从哪个socket的接收缓冲区中取出数据)

buf:用户态缓冲区,存放从接收缓冲区中取出的数据

buflen:想要获取的数据长度

flag:操作选项,默认为0阻塞接收(缓冲区中没有数据则阻塞等待)

peer_addr:地址缓冲区首地址,获取发送这个数据的源端地址信息

addr_len:指定想要获取地址信息的长度以及返回实际获取的长度

  1. 发送数据

ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr* addr dest_addr, socklen_t addrlen)

sockfd:指定内核中socket结构体,绑定的地址信息做为数据中的源信息对数据进行描述

data/datalen:要发送的数据以及数据长度

flag:默认为0阻塞发送数据,若发送缓冲区中数据饱和则进行等待

dest_addr:目的端的地址信息

addr_len:地址信息长度

  1. 关闭套接字

int close(int fd)

2.2 UDP套接字代码实现
  • 主机字节序到网络字节序的转换接口(端口):

32位整数:uint32_t htonl(uint32_t hostlong)
16位整数:uint16_t htons(uint16_t hostshort)

  • 网络字节序到主机字节序的转换接口(端口):

32位整数:uint32_t ntohl(uint32_t netlong)
16位整数:uint16_t ntohs(uint16_t netshort)

  • 将字符串的点分十进制IP地址转换为网络字节序的整数IP地址:

int_addr_t inet_addr(const char* cp)

  • 将网络字节序的整数IP地址转换为字符串的点分十进制IP地址:

char* inet_ntoa(struct in_addr in)

  • 将字符串的IP地址转换为网络字节序的整数IP地址:

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

  • 将网络字节序的整数IP地址转化为字符串的IP地址:

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

UDPsocket.hpp:C++封装一个UDPsocket类

// 使用C++封装一个UDPsocket类
// 实例化出的每一个对象都是一个UDP通信套接字
// 并且通过成员函数实现UDP通信流程

#include<cstdio>
#include<string>
#include<sys/socket.h> // 套接字接口信息
#include<netinet/in.h> // 包含地址结构
#include<arpa/inet.h>
#include<unistd.h>

using namespace std;

class UDPsocket{
public:
    // 构造函数
    UDPsocket()
        :_sockfd(-1)
    {}
    // 1.创建套接字
    bool Socket(){
        _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if(_sockfd < 0){
            perror("socket error");
            return false;
        }
        return true;
    }
    
    // 2.为套接字绑定地址信息
    bool Bind(const string& ip, uint16_t port){
       // 定义IPV4地址结构
       struct sockaddr_in addr;
       addr.sin_family = AF_INET;
       // 将主机字节序短整型转换为网络字节序短整型
       addr.sin_port = htons(port);
       // 将字符串ip地址转换为网络字节序ip地址
       addr.sin_addr.s_addr = inet_addr(ip.c_str());
       // 绑定
       socklen_t len = sizeof(struct sockaddr_in);
       int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
       if(ret < 0){
           perror("bind error");
           return false;
       }
       return true;
    }

    // 3.接收数据并且获取发送端的地址信息
    bool Recv(string* buf, string* ip = NULL, uint16_t* port = NULL){
        struct sockaddr_in peer_addr;
        socklen_t len = sizeof(struct sockaddr_in);
        char tmp[4096] = {0};
        int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
        if(ret < 0){
            perror("recv error");
            return false;
        }
        // 从tmp中截取ret个字节放到buf中
        buf->assign(tmp, ret);
        if(port != NULL){
            // 网络字节序转为主机字节序
            *port = ntohs(peer_addr.sin_port);
        }
        if(ip != NULL){
            // 网络字节序到字符串IP地址的转换
            *ip = inet_ntoa(peer_addr.sin_addr);
        }
        return true;
    }

    // 4.发送数据
    bool Send(const string& data, const string& ip, const uint16_t port){
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("send error");
            return false;
        }
        return true;
    }

    // 5.关闭套接字
    bool Close(){
        if(_sockfd > 0){
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }
private:
    // UDP通信套接字描述符
    int _sockfd;
};

UDPsrv.cc:UDP服务端

#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q)==false){return -1;}
int main(int argc, char* argv[]){
    // argc表示程序运行参数的个数
    if(argc != 3){
        cout << "Usage:./UDPsrv IP Port" << endl;
        return -1;
    }
    uint16_t port = stoi(argv[2]);
    string ip = argv[1];

    UDPsocket srvsock;
    // 1.创建套接字
    CHECKRET(srvsock.Socket());
    // 2.为套接字绑定地址信息
    CHECKRET(srvsock.Bind(ip, port));
    
    while(1){
        // 3.接收数据
        string buf;
        string peer_ip;
        uint16_t peer_port;
        CHECKRET(srvsock.Recv(&buf, &peer_ip, &peer_port));
        cout << "client[" << peer_ip << ":" << peer_port << "]say:" << buf << endl;

        // 4.发送数据
        buf.clear();
        cout << "server say: ";
        cin >> buf;
        CHECKRET(srvsock.Send(buf, peer_ip, peer_port));
    }

    // 5.关闭套接字
    srvsock.Close();
    return 0;
}

UDPcli.cc:UDP客户端

#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q) == false){return -1;}

int main(int argc, char* argv[]){
    // 客户端获取的IP地址是服务端绑定的,也就是客户端发送的目标地址
    if(argc != 3){
        cout << "Usage: ./UDPcli ip port" << endl;
        return -1;
    }
    string srv_ip = argv[1];
    uint16_t srv_port = stoi(argv[2]);

    UDPsocket clisock;
    // 1.创建套接字
    CHECKRET(clisock.Socket());
    // 2.为套接字绑定地址信息(不推荐主动绑定)
    while(1){
        // 3.发送数据
        cout << "client say:";
        string buf;
        cin >> buf;
        CHECKRET(clisock.Send(buf, srv_ip, srv_port));
        // 4.接收数据
        buf.clear();
        CHECKRET(clisock.Recv(&buf));
        cout << "server say: " << buf << endl;
    }

    // 5.关闭套接字
    clisock.Close();
    return 0;
}

三:TCP通信流程

在这里插入图片描述
注意:TCP通信需要建立连接,监听套接字在收到客户端的连接请求后,才会创建通信套接字用于指定客户端和服务端的通信,通信套接字同时包含源端地址信息和对端地址信息,收发数据没有固定的先后顺序。

3.1 TCP套接字相关接口
  1. 创建套接字

接口与UDP通信相同,SOCK_STREAM为流式套接字,默认TCP协议

  1. 为套接字绑定地址信息

接口与UDP通信相同

  1. 开始监听

listen(int sockfd, int backlog)

sockfd:套接字描述符(设置此套接字为监听状态,并且开始接收客户端的连接请求)

backlog:同一时间的并发连接数

  1. 获取新建连接

从已完成连接的套接字队列中取出一个socket,并且返回这个socket的描述符

int accept(int sockfd, struct sockaddr* cli_addr, socklen_t len)

sockfd:监听套接字描述符(获取哪个服务端套接字的新建连接)

cli_addr / len:新建套接字对应的客户端地址信息以及地址信息长度

返回值:新建套接字的套接字描述符

  1. 收发数据

TCP通信套接字中已经包含了源端地址信息和对端地址信息,所以在接收数据的时候不需要获取对方的地址信息,发送数据的时候也不需要指定对端地址信息。

ssize_t recv(int sockfd, char* buf, int len, int flag):默认阻塞,缓冲区没有数据则等待,连接断开返回0
ssize_t send(int sockfd, char* data, int len, int flag):默认阻塞,缓冲区满则等待,连接断开触发SIGPIPE异常

  1. 关闭套接字

close(fd)

  1. 客户端向服务端发起连接请求

int connect(int sockfd, struct sockaddr* srv_addr, int len)

srv_addr:服务端地址信息

connect也会在套接字socket中描述对端地址信息

3.2 TCP套接字代码实现

TCPsocket.hpp:C++封装一个TCPsocket类

#include<cstdio>
#include<string>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define MAX_LISTEN 5
class TCPsocket{
public:
    // 构造函数
    TCPsocket()
        :_sockfd(-1)
    {}

    // 1.创建套接字
    bool Socket(){
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(_sockfd < 0){
            perror("socket error");
            return false;
        }
        return true;
    }

    // 2.为套接字绑定地址信息
    bool Bind(const string& ip, uint16_t port){
        // 组织地址结构IPV4
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("bind error");
            return false;
        }
        return true;
    }

    // 3.开始监听
    bool Listen(int backlog = MAX_LISTEN){
        int ret = listen(_sockfd, backlog);
        if(ret < 0){
            perror("listen error");
            return false;
        }
        return true;
    }

    // 4.获取新建连接
    bool Accept(TCPsocket* new_sock, string* ip = NULL, uint16_t* port = NULL){
        struct sockaddr_in addr;
        socklen_t len = sizeof(struct sockaddr_in);
        int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
        if(new_fd < 0){
            perror("accept error");
            return false;
        }
        new_sock->_sockfd = new_fd;
        if(ip != NULL){
            *ip = inet_ntoa(addr.sin_addr);
        }
        if(port != NULL){
            *port = ntohs(addr.sin_port);
        }
        return true;
    }

    // 5.接收数据
    bool Recv(string* buf){
        char tmp[4096] = {0};
        int ret = recv(_sockfd, tmp, 4096, 0);
        if(ret < 0){
            perror("recv error");
            return false;
        }
        else if(ret == 0){
            printf("disconnected\n");
            return false;
        }
        
        buf->assign(tmp, ret);
        return true;
    }

    // 6.发送数据
    bool Send(const string& data){
        int ret = send(_sockfd, data.c_str(), data.size(), 0);
        if(ret < 0){
            perror("send error");
            return false;
        }
        return true;
    }

    // 7.关闭套接字
    bool Close(){
        if(_sockfd > 0){
            close(_sockfd);
            _sockfd = -1;
        }
        return true;
    }

    // 8.发送连接请求
    bool Connect(const string& ip, uint16_t port){
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
        if(ret < 0){
            perror("connect error");
            return false;
        }
        return true;
    } 
        
private:
    int _sockfd;
};

TCPsrv.cc:TCP服务端

#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Usage: ./TCPsrv ip port" << endl;
        return -1;
    }

    string ip = argv[1];
    uint16_t port = stoi(argv[2]);

    // 创建套接字
    TCPsocket listen_sock;
    CHECK_RET(listen_sock.Socket());

    // 绑定地址信息
    CHECK_RET(listen_sock.Bind(ip, port));

    // 开始监听
    CHECK_RET(listen_sock.Listen());

    while(1){
        TCPsocket new_sock;
        bool ret = listen_sock.Accept(&new_sock);
        if(ret == false){
            // 服务端不能因为获取一个新建套接字失败就退出
            continue;
        }
        string buf;
        new_sock.Recv(&buf);
        cout << "client say: " << buf << endl;

        buf.clear();

        cout << "server say: " << endl;
        cin >> buf;
        new_sock.Send(buf);
    }

    listen_sock.Close();
    return 0;
}

TCPcli.cc:TCP客户端

#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Usage: ./TCPcli ip port" << endl;
        return -1;
    }

    string ip = argv[1];
    uint16_t port = stoi(argv[2]);

    TCPsocket sock;
    CHECK_RET(sock.Socket());
    CHECK_RET(sock.Connect(ip, port));

    while(1){
        string buf;
        cout << "client say: " << endl;
        cin >> buf;
        sock.Send(buf);

        buf.clear();

        sock.Recv(&buf);
        cout << "server say: " << buf << endl;

    }
    sock.Close();
    return 0;
}

我们很容易发现一个问题:

本次实现的TCP通信流程只能完成一个客户端与服务端的一次通信,因为服务端的开始监听和收发数据放在同一个while死循环中,数据一次的收发结束,就会回到监听阶段,阻塞等待新连接的到来,从而只能完成一个客户端与服务端的一次通信。

解决办法:

父进程用于监听操作,阻塞等待新连接的到来

子进程用于指定客户端与服务端的通信,父子进程互不影响。

注意:

子进程退出时,父进程需要循环非阻塞等待子进程的退出,避免产生僵尸进程。

并且这里还需要处理SIGCHILD信号(不可靠信号)为自定义,直到有子进程退出时才循环非阻塞处理,在一次处理中必须处理到没有子进程退出才可以,避免信号的丢失。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值