Linux 网络编程套接字(socket) (网络字节序/UDP/TCP)

目录

认识TCP协议与UDP协议

网络字节序

字节序转换接口

套接字(socket) 

sockaddr结构

socket通信接口

UDP通信流程(客户端)

UDP通信流程(服务端)

TCP通信流程(服务端)

TCP通信流程(客户端)

TCP通信中新的接口

多进程版本通用服务/客户端封装

多线程版通用服务/客户端

TCP和UDP的对比

TCP与UDP的应用场景


认识TCP协议与UDP协议

TCP协议(Transmission Control Protocol, 传输控制协议), 先了解, 下面再详细讨论.

  • 传输层协议
  • 有链接
  • 可靠传输
  • 面向字节流

应用场景 : 安全性高于实时性的场景, 如: 文件传输

UDP协议(User Datagram Protocol, 用户数据报协议), 先了解, 后面再详细讨论

  • 传输层协议
  • 无链接
  • 不可靠传输
  • 面向数据报

应用场景 : 实时性高于安全性的场景, 如 : 视频通话


网络字节序

字节序 :CPU对内存中数据的存取顺序, 取决于CPU的架构

主机字节序 : 有大小端之分, 即小端:低地址存低位, 大端:低地址存高位

网络字节序 : 因为我们用的计算机有大小端之分, 所以大端计算机和小端计算机之间的通信就需要转换, 为了统一标准,
                     TCP/IP协议规定网络字节序为大端序. 

即, 不管这台主机字节序是大端还是小端, 都会按照规定的网络字节序来发送接收数据, 那对于小端序的计算机来说, 接收和发送都需要将数据转换成大端, 对大端的计算机来说则直接忽略, 直接收发.

字节序转换接口

#include<arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htnos(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

开头字母 : h(host)开头是主机字节序转网络字节序,  n(network)开头是网络字节序转主机字节序.

末尾字母: l 表示32位, s 表示16位

字节序转换接口都在 #include<arpa/inet.h> 头文件中


套接字(socket) 

在内核中有一种叫struct socket的数据结构. 进程可以通过struct socket结构体来访问linux内核中的网络系统中的传输层、网络层、数据链路层. 也可以说struct socket是内核中的进程与内核中的网路系统的桥梁.

为了进程能进行各种关于网络通信的操作, 操作系统也提供了一套网络通信的API给用户使用.

sockaddr结构

socket API是一层抽象的网络编程接口, 适用于各种低层网络协议, 如IPv4, IPv6等, 然而, 各种网络协议的地址格式却并不相同, 为了保证接口的统一性, 在需要地址信息的接口中, 统一使用struct aockaddr 类型, 即这个类型的结构体中就是绑定的地址信息.  但刚也说到, 各种网络协议的地址格式不同, 所以struct scokaddr只是一个通用格式, 我们在实际传入的时候传入的是真正的相应的地址结构, 只是将其类型转化成了struct sockaddr, 这样就保证了接口的统一性. 如下图:

地址结构包含在 #include<netinet/in.h> 头文件中

//IPv4
struct in_addr {
    in_addr_t s_addr;
};

struct sockaddr_in {
    sa_family_t sin_family;
    in_port_t sin_port;
    struct in_addr sin_addr;
};

//IPv6
struct in6_addr {
    uint8_t s6_addr[16];   
};

struct sockaddr_in6 {
    sa_family_t sin6_family;  
    in_port_t sin6_port;      
    uint32_t sin6_flowinfo;   
    struct in6_addr sin6_addr;
    uint32_t sin6_scope_id;  
};

                                      

说到这儿再看一下IPv4地址的转换接口, 我们能看到的IPv4地址是 "点分十进制", 也就是一个字符串, 但其本质是一个32位的无符号整数, 也就是IPv4的地址在网络传输和计算机中都是32位无符号整型, 这时就需要转换函数来实现整数和 "点分十进制" 字符串之间的相互转换

#include<arpa/inet.h>

int inet_pton(int af, const char* src, void* dst); 字符串转换整数
    返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1

const char *inet_ntop(int af, const void* src, char* dst, socklen_t size);整数转换"点分十进制"字符串
    返回值:若成功则为指向结构的指针,若出错则为NULL

注意 : 这两个函数不只能转换IPv4的地址, 还可以转换IPv6的地址, 由第一个参数控制, af = AFINET时为IPv4的转换  af = AF_INET6 时, 为IPv6的转换.
inet_pton() :
  参数 :  src :
指向需要被转换的ip地址的指针
             dst:将结果存放到dst所指向的内存中,传入的是其对应的地址结构,IPv4传入的是 struct sockaddr_in地址结构的地址

inet_ntop() : 
  参数 : src :
传入地址信息结构中的IP字段的首地址
            dst :  传入需要存储IP地址信息的缓冲区首地址
            size : IP地址的长度

string ip; 
struct sockaddr_in addr;
char str[INET_ADDRSTRLEN];//IPv6用INET6_ADDRSTRLEN
ip = inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));

socket通信接口

首先连接两个概念, 客户端和服务端

客户端 : 通信起始时先主动发送请求的一端, 客户端在发送请求前就已经提前知道了服务端的地址信息

服务端 : 通信起始时先被动接受请求的一端, 服务端的地址信息早就告知了客户端, 当客户端发送请求给服务端时, 服务端在
              接收客户端数据时, 就获取了客户端的地址信息, 服务端响应客户端时时就知道给谁发送了

就拿我们最常用的QQ来说, 我们两个人之间聊天, 并不是我们两个人的手机或电脑直接通信. 我们手机或电脑的QQ客户端中, 从我们下载安装, 客户端中就已经有了QQ的服务器的地址信息(一般是不会变的), 当我们启动QQ客户端, 如果网络通畅, 我们的QQ客户端会主动给QQ服务器发送数据, 这时服务器就知道了我们的地址信息, 当我们给某个好友发信息, 其实是给QQ服务端发信息, 当QQ服务器收到了我们的消息后(这个消息中还包含我自己的QQ号和好友的QQ号),  服务器先看对方的QQ有没有上线, 如果上线, 那么肯定是知晓其地址信息的(因为客户端会首先发送请求给服务端, 服务端就知晓了客户端的地址信息), 服务器把我们的消息封装后发送给对方, 如果不在线, 那么服务器先保存消息, 等到对方好友上线之后对方好友的客户端给服务器发送请求之后, 服务器知这个好友客户端其地址信息, 就会将保存的信息发送给这个好友.

UDP通信流程(客户端)

  • 1. 创建套接字 : socket()函数, 在内核中创建一个socket结构体, 返回一个文件描述符作为操作句柄, 使进程与网卡之间
                              建立联系

  • 2. 为创建的套接字绑定地址信息 :bind()函数,  给刚在内核中创建的socket结构体中绑定地址信息, 好让操作系统直到自
         己使用了哪个地址和端口号, 当操作系统收到相应的网络数据之后, 就可以通过网络数据中的ip和端口号, 将数据放到
         相对应的socket的接收缓冲区中, 对应的进程就会取出数据并处理.

         注意 : 绑定地址信息的操作在客户端中一般不手动实现, 因为客户端中的地址信息不需要固定(因为当客户端首先给
         服务端发送请求后, 服务端就会知道客户端的地址信息, 所以不需要固定), 但如果我们绑定确定的地址信息时, 地址
         信息中的端口号就会存在冲突的可能, 所以我们不手动绑定, 但操作系统会自动绑定地址信息(自动分配端口号, 避免
         冲突)
  • 3. 发送数据 : sendto()函数, 将数据拷贝到内核中的socket中的发送缓冲区中( 操作系统会根据协议对数据进行层层封装
        之后发送出去)
  • 4. 接收数据 : recvfrom()函数 ,进程通过操作句柄从内核的socket接收缓冲区中取出已经接收到的数据(操作系统已经在
        完成了传输层以下的数据分用)
  • 5. 关闭套接字 : close()函数, 关闭文件描述符, 释放资源

UDP通信流程(服务端)

  • 1. 创建套接字 :socket()函数
  • 2. 为创建的套接字绑定地址信息 : bind()函数,
  • 3. 接收数据 : recvfrom()函数
  • 4. 发送数据 : sendto()函数
  • 5. 关闭套接字 : close()函数

除了关闭套接字的文件描述符的接口close()之外, 其余套接字的接口都包含在  #include<sys/socket.h>

1. 创建套接字接口

int socket(int domain, int type, int protocol)

参数:  domain : 地址域 , 标识什么样的地址信息结构 , 如 : 传入AF_INET为 IPv4的网络协议,. 传入AF_INET6为IPv6的网
                     络协议,  还有其他协议
           type : 套接字类型 : 决定提供什么样的传输服务.
                     SOCK_STREAM : 流式套接字
                     SOCK_DGRAM : 数据报套接字, 还有其他类型
           protocol : 协议类型 : 传入0为默认协议, 流式套接字默认是TCP协议, 数据报套接字默认是UDP协议. 也可以直接传
                     入, TCP协议为IPPROTO_TCP, UDP协议为PPROTO_UDP.

返回值 : 成功返回新创建的socket的文件描述符, 是一个数组下标是>0的, 失败返回-1, 并设置errno

2. 为套接字绑定一个地址信息

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

参数 : sockfd : socket()函数返回的套接字的操作句柄(文件描述符)
           addr : 地址信息结构的首地址, 用于绑定内核中socket中所使用的地址信息. 例如IPv4; 如下:

string ip;IPv4的IP地址
uint16_t port;端口号
struct sockaddr_in addr;//定义IPv4的地址结构
//给addr中绑定地址信息与端口号
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);

int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

         len : 地址信息结构长度, bind接口是统一的地址接口, 但是IP协议不同, 地址结构也不同, 所以addr传入的是自己选择的地址结构, 那么也需要传入其长度.

3.发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)

参数 : sockfd : socket()函数创建的内核中sockket的操作句柄, 也就是前面socket()函数返回的文件描述符
           buf : 用户自己申请的内存首地址, 存放的是要发送的数据(函数将其拷贝到内核中socket的发送缓冲区中,然后再发送)
           len : 要发送的数据长度, 以字节为单位
           flags : 选项参数, 通常为0选择默认为0,表示阻塞发送数据(若socket的发送缓冲区中数据已满, 则需要等待)
           dest_addr : 目的端的地址信息结构首地址, 告诉socket这个数据要发给谁
           addrlen : 地址信息的长度

返回值 : 成功返回实际发送的长度, 是 >= 0的, 失败返回 -1, 并设置errno

4. 接收数据

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

参数 : sockfd : socket()函数创建的内核中sockket的操作句柄, 也就是前面socket()函数返回的文件描述符
           buf : 用户态缓冲区, 也就是我们用户申请的一块内存, 用于存放从内核中socket中的接收缓冲区中拷贝出来的内容
           len : 我们想要获取的数据长度, 这个长度不能大于buf的长度
           flags : 默认选0, 为阻塞接受, socket接收缓冲区中没有数据时则阻塞等待
           src_addr : 地址结构缓冲区地址, 作为接收端, 获取发送端的地址信息, 直到是谁发送的
           addrlen:输入输出型参数,即指定recvfrom函数想要获取多长的地址信息,并且还返回实际获取的地址信息结构的长度

返回值 : 成功返回实际接收的长度, 是 > 0的, 失败返回 -1, 并设置errno

5. 关闭套接字

int close(int fd)

头文件 : unistd.h

参数 : fd : socket()函数返回的创建的内核中socket结构体的文件描述符


了解完这些接口就来用一下.

先封装udp的操作函数, socket_udp.hpp

#include<iostream>
#include<string>
#include<unistd.h> //close包含在这个头文件中
#include<netinet/in.h>//地址结构体
#include<arpa/inet.h>//字节序转换接口
#include<sys/socket.h>//套接字接口

using namespace std;
#define BACKLOG 10
#define CHECK_RET(ret){if((ret) == false) return -1;} 

class TcpSocket{
    int m_sockfd;

    void Addr(struct sockaddr_in*, const string&, const uint16_t) const;
public:
    TcpSocket():m_sockfd(-1){}
    ~TcpSocket(){Close();}
    bool Socket();
    bool Bind(const string& ip, const uint16_t port) const;
    bool Listen(int backlog = BACKLOG) const;
    bool Connect(const string& ip, const uint16_t port) const;
    bool Accept(TcpSocket* sock, string* ip = nullptr, uint16_t* port = nullptr) const;
    bool Recv(string* buf) const;
    bool Send(const string& data) const;
    bool Close() const;
};

bool TcpSocket::Socket(){
    m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(m_sockfd < 0){
        perror("socket error");
        return false;
    }
    return true;
}
void TcpSocket::Addr(struct sockaddr_in* addr, const string& ip, const uint16_t port) const{
    addr->sin_family = AF_INET;
    addr->sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &addr->sin_addr.s_addr);
}
bool TcpSocket::Bind(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;
    Addr(&addr, ip, port);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(m_sockfd, (struct sockaddr*)&addr, len);
    if(ret < 0){
        perror("bind error");
        return false;
    }
    return true;
}

bool TcpSocket::Listen(int backlog) const{
    int ret = listen(m_sockfd, backlog);
    if(ret < 0){
        perror("listen error");
        return false;
    }
    return true;
}

bool TcpSocket::Connect(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;//定义一个IPv4的地址结构
    Addr(&addr, ip, port);//向这个结构中绑定地址与端口
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = connect(m_sockfd, (struct sockaddr*)&addr, len);
    //将服务端信息描述到socket中, 并向服务端发起连接请求
    if(ret < 0){
        perror("connect error");
        return false;
    }
    return true;
}

bool TcpSocket::Accept(TcpSocket* sock, string* ip, uint16_t* port) const{
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    int newsockfd = accept(m_sockfd, (struct sockaddr*)&cli_addr, &len);
    if(newsockfd < 0){
        perror("accept error");
        return false;
    }
    sock->m_sockfd = newsockfd;
    char str[INET_ADDRSTRLEN];//IPv6用INET6_ADDRSTRLEN
    if(ip != nullptr){
       *ip = inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str));
    }
    if(port != nullptr){
        *port = ntohs(cli_addr.sin_port);
    }
    return true;
}

bool TcpSocket::Recv(string* buf) const{
    char tmp[4096] = { 0 };
    ssize_t ret = recv(m_sockfd, tmp, 4095, 0);
    if(ret < 0){
        perror("recv error");
        return false;
    }
    else if(ret == 0){
        cout << "connection break\n";
        return false;
    }
    buf->assign(tmp, ret);
    return true;
}
bool TcpSocket::Send(const string& data) const{
     /*ssize_t ret = send(m_sockfd, data.c_str(), data.size(), 0);
      *if(ret < 0){
      *    perror("send error");
      *    return false;
      *}*/
    //send有这个问题, 比如send有想发送100字节的数据, 但实际可
    //能一次发送的数据并没有100字节, 所以需要循环发送, 如下
    size_t slen = 0, ret = 0;
    size_t size = data.size();
    while(slen < size) {
        ret = send(m_sockfd, &data[slen], size - slen, 0);
        if(ret < 0){
            perror("send error");
            return false;                                                  
        }
        slen += ret;                              
    }
    return true;
}
bool TcpSocket::Close()const {
    close(m_sockfd);
    return true;
}

再封装UDP通用服务器 ser_udp.hpp

#include<iostream>
#include<functional>
#include"socket_udp.hpp"
using namespace std;

typedef function<void (const string& req, string* resp)> Handler;
//req:收到的服务端的请求数据, resp:response客户端要响应服务端的数据(回复的数据) 
class UdpServer{
    UdpSocket m_us;
    string m_ip;
    uint16_t m_port;
public:
    UdpServer(const string ip, const uint16_t port);
    ~UdpServer(){m_us.Close();}
    bool Start(Handler handler);
};

UdpServer::UdpServer(const string ip, const uint16_t port):
    m_ip(ip), m_port(port) {
    m_us.Socket();
}
bool UdpServer::Start(Handler handler){
    string ip;
    uint16_t port;
    m_us.Bind(m_ip, m_port);
    while(1){
        string resp, req;
        if(m_us.Recv(&req, &ip, &port) == false){
            return false;
        }
        handler(req, &resp);
        if(m_us.Send(resp, ip, port) == false){
            return false;
        }
    }
    return true;
}

再封装udp通用客户端 cli_udp.hpp

#include<iostream>
#include<string>
#include<functional>
#include"socket_udp.hpp"
using namespace std;

typedef function<void (string* resp)> Handler1;
typedef function<void (const string& req, string* resp)> Handler2;
//req:收到的服务端的请求数据, resp:response客户端要响应服务端的数据(回复的数据) 

class UdpClient{
    UdpSocket m_us;
    string m_ip;
    uint16_t m_port;
public:
    UdpClient(const string ip, const uint16_t port);
    ~UdpClient(){m_us.Close();}
    bool Start(Handler1 handler1, Handler2 handler2);
};
UdpClient::UdpClient(const string ip, const uint16_t port):
    m_ip(ip), m_port(port) {
    m_us.Socket();
}
bool UdpClient::Start(Handler1 handler1, Handler2 handler2){
    string req, resp, ip;
    uint16_t port;
    handler1(&resp);
    while(1){
        if(m_us.Send(resp, m_ip, m_port) == false){
            return false;
        }
        if(m_us.Recv(&req, &ip, &port) == false){
            return false;
        }
        handler2(req, &resp);
        req.clear();
    }
    return true;
}

服务端ser_main.cpp

#include<iostream>
#include<string>
#include"ser_udp.hpp"
using namespace std;

void test(const string& req, string* resp){
    cout << "client say: " << req << endl;
    cout << "server say: ";
    getline(cin, *resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input: ./ser_main [ip] [port]\n";
        return -1;
    }
    UdpServer us(argv[1], atoi(argv[2]));
    us.Start(test);
    return 0;
}

客户端cli_main.cpp

#include<iostream>
#include<string>
#include"cli_udp.hpp"
using namespace std;

void test1(string* resp){
    cout << "client say:";
    getline(cin, *resp);
}
void test2(const string& req, string* resp){
    cout << "servers say: " << req << endl;
    test1(resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input: ./cli_main [ip] [port]\n";
        return -1;
    }
    UdpClient uc(argv[1], atoi(argv[2]));
    uc.Start(test1, test2);
    return 0;
}

 Makefile

all:ser_udp cli_udp
ser_udp:ser_udp.cpp socket_udp.hpp
	g++ $^ -o $@ -std=c++11
cli_udp:cli_udp.cpp socket_udp.hpp
	g++ $^ -o $@ -std=c++11


TCP通信流程(服务端)

  • 1. 创建套接字 : socket() 函数, 在内核中创建一个socket结构体, 返回一个文件描述符作为操作句柄, 使进程与网卡之间
                              建立联系
  • 2. 为创建的套接字绑定地址信息 :bind()函数 给刚在内核中创建的socket结构体中绑定地址信息, 好让操作系统直到自
         己使用了哪个地址和端口号, 当操作系统收到相应的网络数据之后, 就可以通过网络数据中的ip和端口号, 将数据放到
         相对应的socket的接收缓冲区中, 对应的进程就会取出数据并处理.
  • 3. 开始监听 : listen()函数, 服务端先创建一个"监听套接字", 告诉操作系统可以开始接收并处理新的连接请求.
  • 4. 阻塞等待新连接到来, 创建通信套接字 : accept()函数, 当监听套接字接收到新的连接请求时, accept()会创建新的 "通
        信套接字",  用来进行与客户端之间的通信, ("监听套接字" 和 "通信套接字" 都是内核中的socket, 只是因为在TCP通信
        中功能不同取的名字). 新的"通信套接字" 是拷贝了监听套接字, 也就是说此时的通信套接字也拥有了源端的地址信息
        和新连接请求中对端(客户端)的地址信息, 这样通信套接字就与客户端建立了连接, 之后与这个客户端之间的通信都是
        这个通信套接字负责. 当再有新的连接请求到来,重复此过程.

      监听套接字 : 只负责监听客户端新的连接请求 
      通信套接字 : 只负责一个连接的通信

  • 5. 接收数据 :recv()函数 ,新连接到来后创建的新的套接字进行通信,所以recv函数中的sockfd是通讯套接字的操作句柄
  • 6. 发送数据 :send()函数 , 也是用新创建的套接字进行通信,所以send函数中的sockfd是通讯套接字的操作句柄 
  • 7. 关闭套接字: close()函数, 关闭文件描述符, 释放资源

TCP通信流程(客户端)

  • 1. 创建套接字 : 
  • 2. 为套接字绑定地址信息 :  
          注意 : 绑定地址信息的操作在客户端中一般不手动实现, 因为客户端中的地址信息不需要固定(因为当客户端首先给服务端发送请求后, 服务端就会知道客户端的地址信息, 所以不需要固定), 但如果我们绑定确定的地址信息时, 地址信息中的端口号就会存在冲突的可能, 所以我们不手动绑定, 但操作系统会自动绑定地址信息(自动分配端口号, 避免冲突)
  • 3. 向服务端发起连接请求: connect()函数, 客户端向服务端发起连接请求 , 若没有绑定原地址信息, 操作系统会自动绑
            定到sicket中
  • 4. 发送数据 : send()函数
  • 5. 接收数据 : recv()函数 
  • 6. 关闭套接字 : close()函数

TCP通信中新的接口

监听接口

 int listen(int sockfd, int backlog)

功能 : 告诉操作系统, 可以开始接收并处理新的连接请求

参数 : sockfd : 监听套接字的操作句柄
          backlog : 限制内核中同一时间最大的并发连接数(也就是说同一时间能够接受多少个客户端的连接请求)

在Linux中对于一个监听套接字, 内核都维护着两个socket队列, 未完成连接队列和已完成连接队列

                                          

TCP通信是面向连接, 通信之前先要建立连接, 而建立连接是一个过程, 并不是有新连接来就一定可以立刻完成连接. 当一个新的连接请求到来时, 为其创建新的socket是SYN RECEIVED状态的未完成连接的, 先入未完成连接队列, 当完成连接后, 出未完成队列, 入已完成连接队列(这时socket已经是ESTABLISHED状态了).  在Linux2.2以后, backlog是指定已完成连接队列的大小的. 当已完成队列满了时, 新的连接请求就会被丢弃, 也就是客户端的这次连接失败了(如果客户端支持重试, 可能还会成功). accept()系统调用就仅仅从已完成队列中取出socket就可以了, 而不用关心其状态. 而此时listen系统调用的backlog参数用来决定已完成队列的大小.

如果有一个socket在未完成队列中, 一直无法完成连接, 服务端等待超时后就会释放掉这个socket. 但如果当已完成对列已满, 那么未完成队列中的socket已经完成连接, 但也无法如已完成对列, 所以还会在未完成对列中, 并不会释放 .

客户端向服务端发起连接

 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

参数 : sockfd : 客户端创建的套接字操作句柄, 若没有给这个socket绑定地址信息, 操作系统会自动绑定, 并秒入到socket中
           addr : 服务端的地址信息
           addrlen : 地址信息结构的长度

返回值 : 成功返回0, 失败返回-1, 并设置errno

阻塞等待新连接到来, 创建通信套接字

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

参数 : sockfd : 监听套接字描述符, 指定从内核中哪个监听套接字的队列
           addr : 取出新创建的socket的对端(客户端)地址信息
           len : 输入输出型参数, 指定想要的地址数据长度, 并且获取其实际的地址数据的长度

接收数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags)

参数 : sockfd : accept()函数返回的通信套接字操作句柄
           buf : 接收数据的用户缓冲区, 从内核中socket的接收缓冲区中的数据拷贝到buf所指向的空间
           len : 需要接收的字节数, 不能大于buf的长度
           flags : 默认置0, 阻塞接收, socket接收缓冲区空的话, recv阻塞

发送数据

int send(int sockfd, const void *msg, size_t len, int flags);

参数 : sockfd : accept()函数返回的通信套接字操作句柄
          msg : 发送数据的用户缓冲区, 把msg中的数据拷贝到内核中socket中的发送缓冲区中的
          len : 需要发送的数据长度
          flags : 默认置0, 阻塞发送, socket发送缓冲区为空, send阻塞

了解完就用一下

先封装接口, socket_tcp.hpp

#include<iostream>
#include<string>
#include<unistd.h> //close包含在这个头文件中
#include<netinet/in.h>//地址结构体
#include<arpa/inet.h>//字节序转换接口
#include<sys/socket.h>//套接字接口

using namespace std;
#define BACKLOG 10
#define CHECK_RET(ret){if((ret) == false) return -1;} 

class TcpSocket{
    int m_sockfd;

    void Addr(struct sockaddr_in*, const string&, const uint16_t) const;
public:
    TcpSocket():m_sockfd(-1){}
    bool Socket();
    bool Bind(const string& ip, const uint16_t port) const;
    bool Listen(int backlog = BACKLOG) const;
    bool Connect(const string& ip, const uint16_t port) const;
    bool Accept(TcpSocket* sock, string* ip = nullptr, uint16_t* port = nullptr) const;
    bool Recv(string* buf) const;
    bool Send(const string& data) const;
    bool Close() const;
};

bool TcpSocket::Socket(){
    m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(m_sockfd < 0){
        perror("socket error");
        return false;
    }
    return true;
}
void TcpSocket::Addr(struct sockaddr_in* addr, const string& ip, const uint16_t port) const{
    addr->sin_family = AF_INET;
    addr->sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &addr->sin_addr.s_addr);
}
bool TcpSocket::Bind(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;
    Addr(&addr, ip, port);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(m_sockfd, (struct sockaddr*)&addr, len);
    if(ret < 0){
        perror("bind error");
        return false;
    }
    return true;
}

bool TcpSocket::Listen(int backlog) const{
    int ret = listen(m_sockfd, backlog);
    if(ret < 0){
        perror("listen error");
        return false;
    }
    return true;
}

bool TcpSocket::Connect(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;//定义一个IPv4的地址结构
    Addr(&addr, ip, port);//向这个结构中绑定地址与端口
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = connect(m_sockfd, (struct sockaddr*)&addr, len);
    //将服务端信息描述到socket中, 并向服务端发起连接请求
    if(ret < 0){
        perror("connect error");
        return false;
    }
    return true;
}

bool TcpSocket::Accept(TcpSocket* sock, string* ip, uint16_t* port) const{
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    int newsockfd = accept(m_sockfd, (struct sockaddr*)&cli_addr, &len);
    if(newsockfd < 0){
        perror("accept error");
        return false;
    }
    sock->m_sockfd = newsockfd;
    char str[INET_ADDRSTRLEN];//IPv6用INET6_ADDRSTRLEN
    if(ip != nullptr){
       *ip = inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str));
    }
    if(port != nullptr){
        *port = ntohs(cli_addr.sin_port);
    }
    return true;
}

bool TcpSocket::Recv(string* buf) const{
    char tmp[4096] = { 0 };
    ssize_t ret = recv(m_sockfd, tmp, 4095, 0);
    if(ret < 0){
        perror("recv error");
        return false;
    }
    else if(ret == 0){
        cout << "connection break\n";
        return false;
    }
    buf->assign(tmp, ret);
    return true;
}
bool TcpSocket::Send(const string& data) const{
     /*ssize_t ret = send(m_sockfd, data.c_str(), data.size(), 0);
      *if(ret < 0){
      *    perror("send error");
      *    return false;
      *}*/
    //send有这个问题, 比如send有想发送100字节的数据, 但实际可
    //能一次发送的数据并没有100字节, 所以需要循环发送, 如下
    size_t slen = 0, ret = 0;
    size_t size = data.size();
    while(slen < size) {
        ret = send(m_sockfd, &data[slen], size - slen, 0);
        if(ret < 0){
            perror("send error");
            return false;                                                  
        }
        slen += ret;                              
    }
    return true;
}
bool TcpSocket::Close()const {
    close(m_sockfd);
    return true;
}

ser_main.cpp 

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include"socket_tcp.hpp"

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./ser_tcp [ip] [port]\n";
        return -1;
    }
    TcpSocket ser;
    CHECK_RET(ser.Socket());
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    CHECK_RET(ser.Bind(ip, port));
    CHECK_RET(ser.Listen());
    string buf;
    while(1){
        TcpSocket newsock;
        string ip;
        uint16_t port;
        bool ret = ser.Accept(&newsock, &ip, &port);
        if(ret == false){ continue; }//服务端并不会因为一次失败而退出, 而是继续获取下一个连接
        printf("new connection[ip: %s][port: %d]\n", ip.c_str(), port);  
        ret = newsock.Recv(&buf);
        if (ret == false) {
            break;
        }
        cout << "client say: " << buf << endl;
        cout << "serves say: "; 
        getline(cin, buf);
        ret = newsock.Send(buf);
    }
    ser.Close();
    return 0;
}

cli_main.cpp

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include"socket_tcp.hpp"

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./cli_tcp [ip] [port]\n";
        return -1;
    }
    TcpSocket cli;
    CHECK_RET(cli.Socket());
    cli.Socket();
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    CHECK_RET(cli.Connect(ip, port));
    //开始持续通信
    string buf;
    while(1){
        cout << "client say : ";
        getline(cin, buf);
        CHECK_RET(cli.Send(buf));
        buf.clear();
        CHECK_RET(cli.Recv(&buf));
        cout << "servers say : " << buf << endl;
    }
    cli.Close();
    return 0;
}

Makefile 

all:ser_tcp cli_tcp
ser_tcp:ser_tcp.cpp socket_tcp.hpp
		g++ $^ -o $@ -std=c++11
cli_tcp:cli_tcp.cpp socket_tcp.hpp
		g++ $^ -o $@ -std=c++11

我们发现当一次完整会话之后, 客户端与服务端都卡住了, 这是什么原因呢?

因为在服务端的accept()是一个阻塞函数,  在服务端接收了第一个客户端连接请求, 完成第一次与客户端的接发数据后, 又到了accept()函数处, 这是一个阻塞函数, 此时没有新的连接请求到来, 就会阻塞 . 此时如果来一个新的请求, accept()不再阻塞,  此时服务端执行到recv()处, 如果新连接的客户端没有发送数据, 那么recv()函数也会阻塞, 服务端执行又无法推进. 

其实鬼从根节点, 度无端流程阻塞, 是因为程序并不知道啥时候有新的连接请求到来, 不知道已经连接的客户端啥时候发送来数据, 因此只能固定流程, 先获取连接, 再接收数据, 响应数据: 然而其中的获取连接和接收数据都可能会阻塞.

解决方法 : 多执行流解决方案 也就是 多进程多线程

多进程版本通用服务/客户端封装

TCP通信接口上面已经封装好了, 直接用, 先封装ser_tcp.hpp

#include<iostream>
#include<functional>
#include<sys/wait.h>
#include"socket_tcp.hpp"

typedef std::function<void (const string&, string*, const string&, const uint16_t&)> Handler;

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

class TcpProcessServer{
    TcpSocket m_listen_scok;
    string m_ip;
    uint16_t m_port;

    void ProcessConnect(const TcpSocket& newsock, const string& ip, 
                    const uint16_t port, Handler handler);
public:
    TcpProcessServer(const string ip, const uint16_t port);
    ~TcpProcessServer(){m_listen_scok.Close();}
    bool Start(Handler handler, int backlog = BACKLOG);
};

TcpProcessServer::TcpProcessServer(const string ip, const uint16_t port):
    m_ip(ip), m_port(port) {
    signal(SIGCHLD, sigcb);
    m_listen_scok.Socket();
}

void TcpProcessServer::ProcessConnect(const TcpSocket& newsock, const string& ip,
        const uint16_t port, Handler handler){
    pid_t pid = fork();
    if(pid > 0){
        return;
    }
    else if(pid == 0){
        while(1){
            string req, resp;
            if(newsock.Recv(&req) == false){
                return;
            }
            handler(req, &resp, ip, port);
            if(newsock.Send(resp) == false){
                return;
            }
        }
    }
    else{
        perror("fork error");
        return;
    }
}

bool TcpProcessServer::Start(Handler handler, int backlog){
    CHECK_RET(m_listen_scok.Bind(m_ip, m_port));
    CHECK_RET(m_listen_scok.Listen(backlog));
    while(1){
        TcpSocket newsock;
        string ip;
        uint16_t port;
        if(m_listen_scok.Accept(&newsock, &ip, &port) == false){
            continue;
        }
        printf("new connection[ip: %s][port: %d]\n", ip.c_str(), port);
        ProcessConnect(newsock, ip, port, handler);
    }
    return true;
}

cli_tcp.hpp

#include<iostream>
#include<functional>
#include"socket_tcp.hpp"
using namespace std;

typedef function<void (string* resp)> Handler1;
typedef function<void (const string& req, string* resp)> Handler2;
//req:收到的服务端的请求数据, resp:response客户端要响应服务端的数据(回复的数据)

class TcpClient {
    TcpSocket m_ts;
    string m_ip;
    uint16_t m_port;
public:
    TcpClient(const string ip, const uint16_t port);
    ~TcpClient(){m_ts.Close();}
    bool Strat(Handler1 handler1, Handler2 handler2);
};

TcpClient::TcpClient(const string ip, const uint16_t port):
    m_ip(ip), m_port(port) {
    m_ts.Socket();
}
bool TcpClient::Strat(Handler1 handler1, Handler2 handler2){
    if(m_ts.Connect(m_ip, m_port) == false){
        return false;
    }
    string req, resp;
    handler1(&resp);
    while(1){
        if(m_ts.Send(resp) == false){
            return false;
        }
        if(m_ts.Recv(&req) == false){
            return false;
        }
        handler2(req, &resp);
        req.clear();
    }
    return true;
}

ser_main.cpp

#include<iostream>
#include<string>
#include"ser_tcp.hpp"
using namespace std;

void test(const string& req, string* resp, const string& ip, const uint16_t port){
    printf("client[ip:%s]:[port:%d]: %s\n", ip.c_str(), port, req.c_str());
    cout << "servers say:";
    getline(cin, *resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./ser_main [ip] [port]\n";
        return -1;
    }
    TcpProcessServer ts(string(argv[1]), atoi(argv[2]));
    ts.Start(test);
    return 0;
}

cli_main.cpp

#include<iostream>
#include<string>
#include"cli_tcp.hpp"
using namespace std;

void test1(string* resp){
    cout << "client say: ";
    getline(cin, *resp);
}
void test2(const string& req, string* resp){
    cout << "servers say: " << req << endl;
    test1(resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input: ./cli_main [ip] [port]\n";
        return -1;
    }
    TcpClient tc(argv[1], atoi(argv[2]));
    tc.Strat(test1, test2);
    return 0;
}

Makefile 

all:ser_main cli_main
ser_main:ser_main.cpp ser_tcp.hpp socket_tcp.hpp
    g++ $^ -o $@ -std=c++11
cli_main:cli_main.cpp cli_tcp.hpp socket_tcp.hpp
    g++ $^ -o $@ -std=c++11

多线程版通用服务/客户端

封装服务端 ser_tcp.hpp

#include<iostream>
#include<cstring>
#include<functional>
#include<pthread.h>
#include"socket_tcp.hpp"

typedef function<void (const string&, string*, const string&, const uint16_t)> Handler;

class Data{
public:
    TcpSocket m_newsock;
    string m_ip;
    uint16_t m_port;
    Handler m_handler;
};

class TcpThreadServer{
    TcpSocket m_listen_sock;
    string m_ip;
    uint16_t m_port;

    static void* ThreadConnect(void* arg);
public:
    TcpThreadServer(const string ip, const uint16_t port):
        m_ip(ip), m_port(port) {
        m_listen_sock.Socket();
    }
    ~TcpThreadServer(){m_listen_sock.Close();}
    bool Start(Handler handler, int backlog = BACKLOG);
};
void* TcpThreadServer::ThreadConnect(void* arg){
    pthread_detach(pthread_self());
    Data* data = (Data*)arg;
    string req, resp;
    while(1){
        if(data->m_newsock.Recv(&req) == false) {
            delete data;
            break;
        }
        data->m_handler(req, &resp, data->m_ip, data->m_port);
        if(data->m_newsock.Send(resp) == false){
            delete data;
            break;
        }
    }
    return NULL;
}
bool TcpThreadServer::Start(Handler handler, int backlog){
    CHECK_RET(m_listen_sock.Bind(m_ip, m_port));
    CHECK_RET(m_listen_sock.Listen(backlog));
    while(1){
        Data* data = new Data;
        data->m_handler = handler;
        if(m_listen_sock.Accept(&data->m_newsock, &data->m_ip, &data->m_port) == false){
            delete data;
            continue;
        }
        printf("new connection[ip: %s][port: %d]\n", data->m_ip.c_str(), data->m_port);
        pthread_t tid;
        int ret = pthread_create(&tid, NULL, ThreadConnect, (void*)(data));
        if(ret){
            fprintf(stderr, "pthread_create:%s\n", strerror(ret));
            delete data;
            return false;
        }
    }
    return true;
}

再封装通用客户端cli_tcp.hpp

#include<iostream>
#include<functional>
#include"socket_tcp.hpp"
using namespace std;

typedef function<void (string* resp)> Handler1;
typedef function<void (const string& req, string* resp)> Handler2;
//req:收到的服务端的请求数据, resp:response客户端要响应服务端的数据(回复的数据)

class TcpClient {
    TcpSocket m_ts;
    string m_ip;
    uint16_t m_port;
public:
    TcpClient(const string ip, const uint16_t port);
    ~TcpClient(){m_ts.Close();}
    bool Strat(Handler1 handler1, Handler2 handler2);
};

TcpClient::TcpClient(const string ip, const uint16_t port):
    m_ip(ip), m_port(port) {
    m_ts.Socket();
}
bool TcpClient::Strat(Handler1 handler1, Handler2 handler2){
    if(m_ts.Connect(m_ip, m_port) == false){
        return false;
    }
    string req, resp;
    handler1(&resp);
    while(1){
        if(m_ts.Send(resp) == false){
            return false;
        }
        if(m_ts.Recv(&req) == false){
            return false;
        }
        handler2(req, &resp);
        req.clear();
    }
    return true;
}

ser_main.cpp

#include<iostream>
#include<string>
#include"ser_tcp.hpp"
using namespace std;

void test(const string& req, string* resp, const string& ip, const uint16_t port){
    printf("client[ip:%s]:[port:%d]: %s\n", ip.c_str(), port, req.c_str());
    cout << "servers say:";
    getline(cin, *resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./ser_main [ip] [port]\n";
        return -1;
    }
    TcpThreadServer ts(string(argv[1]), atoi(argv[2]));
    ts.Start(test);
    return 0;
}

cli_main.cpp

#include<iostream>
#include<string>
#include"cli_tcp.hpp"
using namespace std;

void test1(string* resp){
    cout << "client say: ";
    getline(cin, *resp);
}
void test2(const string& req, string* resp){
    cout << "servers say: " << req << endl;
    test1(resp);
}

int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input: ./cli_main [ip] [port]\n";
        return -1;
    }
    TcpClient tc(argv[1], atoi(argv[2]));
    tc.Strat(test1, test2);
    return 0;
}

Makefile

all:ser_main cli_main
ser_main:ser_main.cpp ser_tcp.hpp socket_tcp.hpp
    g++ $^ -o $@ -std=c++11 -pthread
cli_main:cli_main.cpp cli_tcp.hpp socket_tcp.hpp
    g++ $^ -o $@ -std=c++11

TCP和UDP的对比

1.TCP有连接, UDP无连接

TCP通信是有连接的, 也就是在通信前, 要先建立连接, 否则无法通信. 

UDP通信则没有连接, 直接通信

2. TCP字节流, UDP数据报

每一条TCP连接只能是点到点的;  UDP支持一对一, 一对多, 多对一和多对多的交互通信, 这就存在一些问题.

如, TCP面向字节流传输, 是将数据看成一连串无结构的字节流进行传输的 , 一个socket接收缓冲区中的数据都是由唯一确定的主机发送的, 所以只用保证数据的有序性就好了, 就比如发送100字节的数据, 字节流传输可以一次发送100字节, 也可以一次发送1字节, 发送100次. 

而UDP是无连接的协议, 也就是说, 只要知道接收端的IP和端口, 且网络是可达的, 任何主机都可以向接收端发送数据. 这时候, 如果像是字节流传输, 那在接收缓冲区中的数据到底是谁发的, 顺序是什么, 各自是多大, 都难以保证, 就会出现混乱. 所以, UDP面向数据报传输, 一次发送一个报文的数据, 接收端读取时一次也只读一个报文的数据. 保证接收的数据不会混乱, 应用层交给UDP多长的数据, 都不会像TCP那样, 可能出现拆分发送的情况, 而是给大多, 发多大, 所以UDP不适合大数据的发送(因为数据太大一次发送失败的概率很大)

3. 各自优缺点

TCP的优点 : 可靠, (无差错, 不丢失, 不重复, 且按序到达)TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连
                    接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统
                    资源.

TCP的缺点 : 较慢, 效率低, 系统资源占用多,  建立连接过程消耗时间, 传输过程中确认机制、重传机制、拥塞控制机制等都
                    会消耗较多时间, 每台设备都要维护所有的传输连接, 都需要占用资源
                    安全性比UDP较低, 因为TCP有确认机制, 三次握手机制, 这些也导致TCP容易被人利用, 实现DOS, DDOS,
                    CC等攻击

UDP的优点 : 较快, UDP没有TCP的握手, 确认, 窗口, 重传, 拥塞控制等机制, UDP是一个无状态的传输协议, 所以它在传递
                     数据时非常快
                     安全性比TCP较高 , 没有TCP的这些机制, UDP较TCP被攻击者利用的漏洞就要少一些. 但UDP也是无法避免
                     攻击的, 比如 : UDP Flood攻击……

UDP的缺点 : 不可靠, (不保证可靠交付, 不保证顺序)因为UDP没有TCP那些可靠的机制,在数据传递时, 如果网络质量不好,
                     就会很容易丢包.

TCP与UDP的应用场景

基本原则 : 对数据安全可靠性要求高的使用TCP, 如 : HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的
                 协议
                 
                 对数据传输实时性要求高的使用UDP, 如 : 语音电话, 视频电话, 实时聊天等

为什么UDP有时比TCP更有优势?

UDP以其简单, 传输快的优势, 在越来越多场景下取代了TCP, 如多人实时游戏. 原因如下 :

    1. 网速的提升给UDP的稳定性提供可靠网络保障, 丢包率很低, 如果使用应用层重传, 能够确保传输的可靠性. 尤其是现
        在5G网络的发展, 其低丢包率, 低延迟, 高传输速率, 给了UDP更大的空间

    2. TCP为了实现网络通信的可靠性, 使用了复杂的拥塞控制算法, 建立了繁琐的握手过程, 由于TCP内置的系统协议栈
        中,极难对其进行改进.  采用TCP, 一旦发生丢包, TCP会将后续的包缓存起来, 等前面的包重传并接收到后再继续发
        送, 延时会越来越大, 基于UDP对实时性要求较为严格的情况下, 采用自定义重传机制, 能够把丢包产生的延迟降到最
        低, 尽量减少网络问题对游戏等实时性要求较高的场景造成影响 .

发布了232 篇原创文章 · 获赞 720 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/104972236