套接字编程

套接字编程涉及到两种网络传输协议 -- TCP / UDP
所以了解套接字编程之前,得先了解这两种传输协议的区别

TCP(传输控制协议) – 面向连接,可靠传输,面向字节流
UDP(用户数据报协议)–无连接,不可靠,面向数据报
TCP保证可靠安全传输,但是传输速度没有UDP块,所以TCP应用于安全性要求高的场景 / UDP应用于实时性要求高的场景

UDP通信流程

UDP通信流程
那为什么说客户端不推荐主动绑定地址呢?
因为客户端发送数据用什么源端地址无所谓,主动绑定,若被绑定的端口已经被占用,那么绑定就会失败

socket接口介绍
  1. 创建套接字
    int socket(int domain,int type,int protocol)
    domain: 地址域–确定本次socket通信使用哪种协议版本的地址结构 — 不同的协议版本有不同的额地址结构(AF_INET IPV4网络协议)
    type: 套接字类型(流失套接字 – SOCK_STREAM / 数据报套接字 – SOCK_DGRAM)
    protocol: 协议类型(通常就是传输协议的选择 IPPROTO_TCP / IPPROTO_UDP)
    返回值: 文件描述符 – 非负整数 – 套接字所有其他接口的操作句柄,失败返回-1
  2. 为套接字绑定地址信息
    int bind(int sockfd,struct sockaddr *addr,socklen_t len)
    sockfd: 创建套接字返回的操作句柄
    addr: 要绑定的地址信息
    len: 要绑定的地址信息长度
bind可以绑定不同的地址结构,
为了实现统一,因此用户定义地址结构的时候,
定义自己需要的地址结构(例如IPV4就是用struct sockaddr_in),但是进行绑定的时候,
统一类型强转成为sockaddr *类型
bind(fd,struct socketaddr *addr,len) {
	if(addr->sa_family == AF_INET) {
		// 绑定IPV4地址信息,这个结构体按照sockaddr_in进行解析
	}
	else if(addr->sa_family == AF_INET6) {
		// IPV6地址绑定,按照IPV6地址结构解析
	}
	else if(addr->sa_family == AF_LOCAL)
	{}
  1. 接收数据 – 不仅仅接收数据,还要通过这个数据得知这个数据是谁发的,以便进行回复
    int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len)
    sockfd: socket操作句柄
    buf: 一块缓冲区,用于接收从从接受缓冲区中取出的数据
    len: 想要接收的数据长度
    flag: 操作选项标志,默认为0,表示阻塞操作
    peer_addr: 发送方的地址信息
    addrlen: 想要获取的地址信息长度以及返回实际长度
    返回值: 成功返回接收到的数据字节长度,失败返回-1
  2. 发送数据
    ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr* dest_addr, socklen_t addrlen)
    sockfd: socket操作句柄
    data: 要发送的数据首地址
    len: 要发送数据长度
    flag: 默认为0,表示阻塞操作
    desk_addr: 接收方的地址信息
    addrlen: 地址信息长度
    返回值: 成功返回实际发送的数据的字节长度,失败返回-1
  3. 关闭套接字
    int close(fd)
网络字节序转换接口
  • uint32_t htonl(uint32_t hostlong)
  • uint16_t htons(uint16_t hostshort)
  • uint32_t ntohl(uint32_t netlong)
  • 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
    在进行uint16_t类型数据转换的时候,不能使用 ntohl / htonl ,只能使用htons / ntohs
UDPsocket.hpp–实现 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;
};

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;
}

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通信流程

在这里插入图片描述

socket接口
  1. 创建套接字
    int socket(int domain,int type,int protocol)
    type: SOCK_DGRAM – 数据报套接字 / SOCK_STREAM – 流式套接字
    protocol: IPPROTO_TCP
  2. 绑定地址信息
    int bind(int sockfd,struct sockaddr *addr,socklen_t len)
  3. 开始监听
    listen(int sockfd,int backlog)
    sockfd: 将哪个套接字设置为监听状态。并且监听状态后可以开始接收客户端连接请求
    backlog: 同一时间的并发连接数,决定同一时间最多接收多少个客户端的连接请求
  4. 获取新建连接(从已完成连接队列中取出一个socket,并且返回这个socket的描述符操作句柄)
    int accept(int sockfd,struct sockaddr *cli_addr,socklen_t *len)
    sockfd: 监听套接字,表示要获取哪个TCP服务端套接字的新建连接
    cli_addr: 这个新的套接字对应的客户端地址信息
    len: 地址信息长度
    返回值: 新建socket套接字的描述符 – 外部程序中的操作句柄
  5. 接收数据/发送数据
    ssize_t recv(int sockfd, char* buf, int len, int flag)
    默认阻塞,没有数据则等待,连接断开返回0,不再阻塞
    ssize_t send(int sockfd, char* data, int len, int flag)
    默认阻塞,缓冲区数据满了则等待,连接断开则触发SIGPIPE异常
  6. 关闭套接字
    int close(fd)
  7. 客户端向服务端发起连接请求
    int connect(int sockfd, struct sockaddr* srv_addr, int len)
    srv_addr: 服务端地址信息 – 给谁发送连接请求
    connect这个接口也会在sockfd的套接字中描述对端地址信息
TCPsocket.hpp–实现 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;
};

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;
}

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;
}

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值