Linux

    xiaoxiao2022-07-04  218

    一. 主机字节序和网络字节序

    32位机器CPU一次至少装载4字节, 这4字节在内存中的排列顺序就是字节序

    字节序分为大端字节序: 低地址存高位

                       小端字节序:低地址存低位

    利用union验证本机的字节序:

    int main(){ union { char a; int b; } test; test.b = 1; if (test.a == 0) { printf("big endian\n"); //大端字节序 } else if (test.a == 1){ printf("little endian\n"); //小端字节序 } return 0; }

    原理: 

    现代PC大多采用小端字节序, 因此小端字节序又被成为主机字节序

    规定网络字节序为大端字节序, 所有主机收发数据时要转换为大端字节序

    // Linux 提供 4 个函数完成字节序转换 #include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); // n net代表网络, h host代表主机 // l long 32位, 用于 ip地址转换 // s short 16位 用于 端口转换

      二. socket地址表示

       通用socket

    struct sockaddr { sa_family_t sa_family; // 地址族类型 char sa_data[14]; // 存放socket地址值 }

    专用socket 

    struct sockaddr_in { sa_family_t sin_family; // 地址族 uint16_t sin_port; // 端口号 struct in_addr sin_addr;// Ipv4 地址结构体 } struct in_addr { uint32_t s_addr; // Ipv4 地址 }

    写代码用sockaddr_in, 类型转换为 sockaddr

    三.  ip地址转换函数

    通常使用ip地址用点分十进制表示, 在编写代码的时候, 需要把ip转换为32位整数

    #include <arpa/inet.h> in_addr_t inet_addr (const char* strptr); int inet_aton(const char *cp, struct in_addr *inp); char *inet_ntoa(struct in_addr in);

    四. TCP编程流程

    TCP协议: 有连接, 可靠, 面向字节流

    1. 创建 socket                      int socket(int domain, int type, int protocol);

    2. 绑定地址信息(服务端)       int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 

    3. 监听socket()服务端)         int listen(int sockfd, int backlog);     backlog决定了内核中已完成连接队列的最大结点数

    4. 接受连接(服务端)              int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

        调用accept从listen监听队列中接受一个连接, accept成功返回一个新的连接socket, 可通过新socket来与请求连接的

        客户端通信

    5. 发起连接(客户端)               int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

        客户端调用connect主动发起连接

    6. 关闭连接                             int close(int fd);

        close 并非立即关闭一个连接, 而是将 fd 的引用计数 -1, 只有当 fd 为 0 时, 才真正关闭连接

        在多进程程序中, 子进程拷贝了父进程地址空间, 只有父子进程都close了socket, 才能关闭连接

        如果一定要立即终止连接, 使用

        int shutdown(int sockfd, int how);

        how: SHUT_RD 关闭读, SHUT_WR 关闭写, SHUT_RDWR 关闭读写

    TCP数据读写 :

        对文件的读写操作 read和write同样适用于socket, socket编程接口也提供了专门用于socket数据读写的系统调用

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

            recv返回 - 1 出错, 返回大于0 表示实际读取的字节数, 返回 0 表示对方已经断开连接

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

            sned返回实际写入的字节长度

            flags 一般置 0 , MSG_OOB 发送或接受紧急数据, MSG_PEEK 窥探读缓存中的数据, 此次操作不会删除数据

    如何判断连接已经断开:

    原理:

    tcp的连接管理中, 内建有保活机制: 当长时间 没有数据往来时, 每隔一段时间都会向对方发送一个保活探测包, 要求对方回复

    当多次发送的保活探测包都没有响应, 则认为连接断开

    编写代码:

    连接断开, recv 返回为 0 ; send会触发异常SIGPIPE(导致进程退出)

    五. 封装TCP常用操作 

    class TcpSocket{ public: TcpSocket():_fd(-1){} ~TcpSocket(){ } public: // 创建套接字 bool Socket(){ _fd = socket(AF_INET, SOCK_STREAM, 0); if (_fd == -1){ cout << "Socket err" << endl; return false; } return true; } //关闭套接字 bool Close() { close(_fd); cout << "close fd: " << _fd << endl; return true; } // 绑定 ip port bool Bind(string& ip, uint16_t port){ sockaddr_in bindAddr; bindAddr.sin_family = AF_INET; bindAddr.sin_port = htons(port); bindAddr.sin_addr.s_addr = inet_addr(ip.c_str()); if (bind(_fd, (sockaddr*)&bindAddr, sizeof bindAddr) == -1){ cout << "Bind err" << endl; return false; } return true; } // 监听套接字 bool Listen(int num){ if (listen(_fd, num) == -1){ cout << "listen err" << endl; return false; } return true; } // 服务器等待连接 bool Accept(TcpSocket& newSock, string* ip = nullptr, uint16_t* port = nullptr){ sockaddr_in peerAddr; socklen_t sockLen = sizeof peerAddr; int newFd = accept(_fd, (sockaddr*)&peerAddr, &sockLen); if (newFd < 0){ cout << "accept err" << endl; return false; } newSock.SetFd(newFd); if (ip != nullptr){ *ip = inet_ntoa(peerAddr.sin_addr); } if (port != nullptr) { *port = ntohs(peerAddr.sin_port); } return true; } bool Connect(string& ip, uint16_t port){ // 客户端连接 sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(port); servAddr.sin_addr.s_addr = inet_addr(ip.c_str()); if (connect(_fd, (sockaddr*)&servAddr, sizeof servAddr) == -1){ cout << "Connect() error" << endl; return false; } return true; } bool Recv(string& msg){ //接受 char buf[4096] = {0}; //这里有问题: tcp流无数据边界, 不一定会一次接收完 int recvSize = recv(_fd, buf, 4096, 0); if (recvSize < 0){ cout << "recv() err" << endl; return false; } if (recvSize == 0){ return false; } msg.assign(buf, recvSize); return true; } bool Send(string& msg){ // 发送 int ret = send(_fd, msg.c_str(), msg.size(), 0); if (ret < 0){ cout << "send() err" << endl; return false; } return true; } public: void SetFd(int fd){ _fd = fd; } int GetFd(){ return _fd; // socket 文件描述符 } private: int _fd; };

    使用封装的接口实现简单的回声客户端 和 回声服务器(单对单): 

    服务端:

    int main(){ TcpSocket tcp; tcp.Socket(); string ip = "192.168.30.145"; tcp.Bind(ip, 14396); tcp.Listen(5); // 服务器一直运行, 等待客户端连接 while(1) { TcpSocket newSock; string clntIp; uint16_t clntPort; // 没有客户端连接, 将一直在这里阻塞 tcp.Accept(newSock, &clntIp, &clntPort); cout << "Connect: (" << clntIp << "--" << clntPort << ")" << endl; // 持续与连接的客户端收发信息 for(;;){ string msg; if (!newSock.Recv(msg)){ newSock.Close(); break; } cout << "recv: " << msg << endl; newSock.Send(msg); } } tcp.Close(); return 0; }

    客户端: 

    int main(){ TcpSocket tcp; tcp.Socket(); string ip = "192.168.30.145"; tcp.Connect(ip, 14396); // 连接设置ip 端口的客户端 // 循环收发消息 while(1) { string msg; getline(cin, msg); tcp.Send(msg); string resp; tcp.Recv(resp); cout << "recv msg: " << msg << endl; } tcp.Close(); return 0; }

    同一时刻, 只能有一个客户端与服务器通信: 

     

    如果要让服务器端处理多个客户端连接请求:可以使用多线程或多进程, 父进程(主线程) 处理连接请求, 

    与客户端的通信由子进程(子线程)实现. 

     

    最新回复(0)