网络编程 - 套接字

1、预备知识

1.1、理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址;

思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析;

1.2、认识端口号

端口号(port)是传输层协议的内容;

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;

1.3、理解 "端口号" 和 "进程ID"

pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这 两者之间是怎样的关系?

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

1.4、理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要 发给谁";

1.5、认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一 些细节问题:

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

1.6、认识UDP协议

此处也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识:

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

1.7、网络字节序

        内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
将0x1234abcd写入到以0x0000开始的内存中,则结果为:
            big-endian      little-endian
0x0000      0x12            0xcd
0x0001      0x23            0xab
0x0002      0xab            0x34
0x0003      0xcd            0x12

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换;

#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);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数;
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回;

2、socket编程接口

2.1、socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
 socklen_t addrlen);

2.2、sockaddr结构

        socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同;

  •  IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址;
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构

sockaddr_in 结构

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.;

2.3、in_addr结构

typedef uint32_t in_addr_t;
struct in_addr
{
    int_addr_t s_addr;
};

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

3、简单的UDP网络程序

实现一个简单的英译汉的功能

备注: 代码中会用到 地址转换函数 . 参考接下来的章节;

3.1、封装 UdpSocket

udp_socket.hpp

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket {
public: 
     UdpSocket() : fd_(-1) 
     {}

     bool Socket() 
     {
         fd_ = socket(AF_INET, SOCK_DGRAM, 0);
         if (fd_ < 0)
         {
             perror("socket");
             return false;
         }
         return true;
     }

     bool Close() 
     {
         close(fd_);
         return true;
     }

     bool Bind(const std::string& ip, uint16_t port)
     {
         sockaddr_in addr;
         addr.sin_family = AF_INET;
         addr.sin_addr.s_addr = inet_addr(ip.c_str());
         addr.sin_port = htons(port);
         int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
         if (ret < 0) 
         {
             perror("bind");
             return false;
         }
         return true;
     }

     bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) 
     {
         char tmp[1024 * 10] = {0};
         sockaddr_in peer;
         socklen_t len = sizeof(peer);
         ssize_t read_size = recvfrom(fd_, tmp,
         sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
         if (read_size < 0) 
         {
             perror("recvfrom");
             return false;
         }
 // 将读到的缓冲区内容放到输出参数中
         buf->assign(tmp, read_size);
         if (ip != NULL) 
         {
             *ip = inet_ntoa(peer.sin_addr);
         }
         if (port != NULL) 
         {
             *port = ntohs(peer.sin_port);
         }

         return true;
     }

     bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) 
     {
         sockaddr_in addr;
         addr.sin_family = AF_INET;
         addr.sin_addr.s_addr = inet_addr(ip.c_str());
         addr.sin_port = htons(port);
         ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr,/                                 
         sizeof(addr));

         if (write_size < 0) 
         {
             perror("sendto");
             return false;
         }
         return true;
     }
private:
     int fd_;
};

3.2、UDP通用服务器

udp_server.hpp

#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lamda

#include <functional>
typedef std::function<void (const std::string&, std::string* resp)> Handler;
class UdpServer {
public:
     UdpServer() 
     {
         assert(sock_.Socket());
     }

     ~UdpServer() 
     {
         sock_.Close();
     }

     bool Start(const std::string& ip, uint16_t port, Handler handler) 
     {
      // 1. 创建 socket
      // 2. 绑定端口号
         bool ret = sock_.Bind(ip, port);
         if (!ret) 
         {
             return false;
         }
      // 3. 进入事件循环
         for (;;) 
         {
      // 4. 尝试读取请求
             std::string req;
             std::string remote_ip;
             uint16_t remote_port = 0;
             bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
             if (!ret) 
             {
                 continue;
             }
             std::string resp;
      // 5. 根据请求计算响应
             handler(req, &resp);
      // 6. 返回响应给客户端
             sock_.SendTo(resp, remote_ip, remote_port);
             printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,
             req.c_str(), resp.c_str());
         }
         sock_.Close();
         return true;
     }
private:
     UdpSocket sock_;
};

3.3、实现英译汉服务器

以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了;

dict_server.cc

#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>

std::unordered_map<std::string, std::string> g_dict;

void Translate(const std::string& req, std::string* resp) 
{
    auto it = g_dict.find(req);
    if (it == g_dict.end()) 
    {
        *resp = "未查到!";
        return;
    }
    *resp = it->second;
}

int main(int argc, char* argv[]) 
{
     if (argc != 3) 
     {
         printf("Usage ./dict_server [ip] [port]\n");
         return 1;
     }
  // 1. 数据初始化
     g_dict.insert(std::make_pair("hello", "你好"));
     g_dict.insert(std::make_pair("world", "世界"));
     g_dict.insert(std::make_pair("c++", "最好的编程语言"));

  // 2. 启动服务器
     UdpServer server;
     server.Start(argv[1], atoi(argv[2]), Translate);
     return 0;
}

3.4、UDP通用客户端

udp_client.hpp

#pragma once
#include "udp_socket.hpp"
class UdpClient 
{
public:
     UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
     {
         assert(sock_.Socket());
     }

     ~UdpClient() 
     {
         sock_.Close();
     }

     bool RecvFrom(std::string* buf) 
     {
         return sock_.RecvFrom(buf);
     }

     bool SendTo(const std::string& buf) 
     {
         return sock_.SendTo(buf, ip_, port_);
     }
private:
     UdpSocket sock_;
  // 服务器端的 IP 和 端口号
     std::string ip_;
     uint16_t port_;
};

3.5、实现英译汉客户端

#include "udp_client.hpp"
#include <iostream>
int main(int argc, char* argv[]) 
{
     if (argc != 3) 
     {
         printf("Usage ./dict_client [ip] [port]\n");
         return 1;
     }

     UdpClient client(argv[1], atoi(argv[2]));
     for (;;) 
     {
         std::string word;
         std::cout << "请输入您要查的单词: ";
         std::cin >> word;
         if (!std::cin) 
         {
             std::cout << "Good Bye"<< std::endl;
             break;
         }
         client.SendTo(word);
         std::string result;
         client.RecvFrom(&result);
         std::cout << word << " 意思是 " << result << std::endl;
     }
     return 0;
}

3.6、地址转换函数

        本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的 IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

#include<arpa/inet.h>

int inet_aton(const char* strptr, struct in_addr *addrptr);
int_addr_t inet_addr(const char* strptr);
int inet_pton(int family, const char *strptr, void *addrptr);

in_addr转字符串的函数:

char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr;

代码示例:

#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
    strcut sockaddr_in addr;
    inet_aton("127.0.0.1", &addr.sin_addr);
    uint32_t *ptr = (uint32_t*)(&addr.sin_addr);
    printf("addr: %x\n", *ptr);
    printf("addr_str : %s\n", inet_ntoa(addr.sin_addr));
    return 0;
}

3.7、关于inet_ntoa

        inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢?

        man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

[ketil@tz addr_convert]$ ./a.out
ptr1: 255.255.255.255, ptr2: 255.255.255.255

 因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果;

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

多线程调用inet_ntoa代码示例如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void* Func1(void* p) 
{
     struct sockaddr_in* addr = (struct sockaddr_in*)p;
     while (1) 
     {
         char* ptr = inet_ntoa(addr->sin_addr);
         printf("addr1: %s\n", ptr);
     }
     return NULL;
}

void* Func2(void* p) 
{
     struct sockaddr_in* addr = (struct sockaddr_in*)p;
     while (1) 
     {
         char* ptr = inet_ntoa(addr->sin_addr);
         printf("addr2: %s\n", ptr);
     }
     return NULL;
}

int main() 
{
     pthread_t tid1 = 0;
     struct sockaddr_in addr1;
     struct sockaddr_in addr2;
     addr1.sin_addr.s_addr = 0;
     addr2.sin_addr.s_addr = 0xffffffff;
     pthread_create(&tid1, NULL, Func1, &addr1);
     pthread_t tid2 = 0;
     pthread_create(&tid2, NULL, Func2, &addr2);
     pthread_join(tid1, NULL);
     pthread_join(tid2, NULL);
     return 0;
}

4、简单的TCP网络程序

和刚才UDP类似. 实现一个简单的英译汉的功能

4.1、TCP socket API 详解

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中;

4.1.1、socket()

  •  socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果socket()调用出错则返回-1;
  • 对于IPv4, family参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议;
  • protocol参数的介绍从略,指定为0即可;

4.1.2、bind()

  •  服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号; 
  • bind()成功返回0,失败返回-1;
  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号;
  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑 定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址;
  4. 端口号为SERV_PORT, 我们定义为9999;

4.1.3、listen()

  •  listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5);
  • listen()成功返回0,失败返回-1;

4.1.4、accept()

  •  三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

服务器程序结构是这样的:

while(1)
{
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ...
    close(connfd);
}
  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

4.2、封装 TCP socket

tcp_socket.hpp

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

#define CHECK_RET(exp) if (!(exp)) {\
 return false;\
}

class TcpSocket {
public:
     TcpSocket() : fd_(-1) {}

     TcpSocket(int fd) : fd_(fd) {}

     bool Socket() 
     {
         fd_ = socket(AF_INET, SOCK_STREAM, 0);
         if (fd_ < 0) 
         {
             perror("socket");
             return false;
         }
         printf("open fd = %d\n", fd_);
         return true;
     }

     bool Close() const 
     {
         close(fd_);
         printf("close fd = %d\n", fd_);
         return true;
     }

     bool Bind(const std::string& ip, uint16_t port) const 
     {
         sockaddr_in addr;
         addr.sin_family = AF_INET;
         addr.sin_addr.s_addr = inet_addr(ip.c_str());
         addr.sin_port = htons(port);
         int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
         if (ret < 0) {
             perror("bind");
             return false;
         }
         return true;
     }

     bool Listen(int num) const 
     {
         int ret = listen(fd_, num);
         if (ret < 0)
         {
             perror("listen");
             return false;
         }    
         return true;
     }

     bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const
     {
         sockaddr_in peer_addr;
         socklen_t len = sizeof(peer_addr);
         int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
         if (new_sock < 0) 
         {
             perror("accept");
             return false;
         }
         printf("accept fd = %d\n", new_sock);
         peer->fd_ = new_sock;
         if (ip != NULL) 
         {
             *ip = inet_ntoa(peer_addr.sin_addr);
         }
         if (port != NULL) 
         {
             *port = ntohs(peer_addr.sin_port);
         }
         return true;
     }

     bool Recv(std::string* buf) const 
     {
         buf->clear();
         char tmp[1024 * 10] = {0};
      // [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
      // 参考 man 手册 MSG_WAITALL 节. 
         ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
         if (read_size < 0) 
         {
             perror("recv");
             return false;
         }
         if (read_size == 0)
         {
             return false;
         }
         buf->assign(tmp, read_size);
         return true;
     }

     bool Send(const std::string& buf) const 
     {
         ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
         if (write_size < 0) 
         {
             perror("send");
             return false;
         }
         return true;
     }

     bool Connect(const std::string& ip, uint16_t port) const 
     {
         sockaddr_in addr;
         addr.sin_family = AF_INET;
         addr.sin_addr.s_addr = inet_addr(ip.c_str());
         addr.sin_port = htons(port);
         int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
         if (ret < 0) 
         {
             perror("connect");
             return false;
         }
         return true;
     }

     int GetFd() const 
     {
         return fd_;
     }
private:
     int fd_;
};

4.3、TCP通用服务器

tcp_server.hpp

#pragma once
#include <functional>
#include "tcp_socket.hpp"
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
class TcpServer 
{
public:
     TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
     {}

     bool Start(Handler handler) 
     {
      // 1. 创建 socket;
         CHECK_RET(listen_sock_.Socket());
      // 2. 绑定端口号
         CHECK_RET(listen_sock_.Bind(ip_, port_));
      // 3. 进行监听
         CHECK_RET(listen_sock_.Listen(5));
      // 4. 进入事件循环
         for (;;) 
         {
          // 5. 进行 accept
             TcpSocket new_sock;
             std::string ip;
             uint16_t port = 0;
             if (!listen_sock_.Accept(&new_sock, &ip, &port)) 
             {
                 continue;
             }
             printf("[client %s:%d] connect!\n", ip.c_str(), port);
          // 6. 进行循环读写
             for (;;) 
             {
                 std::string req;
              // 7. 读取请求. 读取失败则结束循环
                 bool ret = new_sock.Recv(&req);
                 if (!ret) 
                 {
                     printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
                  // [注意!] 需要关闭 socket
                     new_sock.Close();
                     break;
                 }
              // 8. 计算响应
                 std::string resp;
                 handler(req, &resp);
              // 9. 写回响应
                 new_sock.Send(resp);
                 printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port,
                 req.c_str(), resp.c_str());
             }
         }
         return true;
     }

private:
     TcpSocket listen_sock_;
     std::string ip_;
     uint64_t port_;
};

4.4、英译汉服务器

#include <unordered_map>
#include "tcp_server.hpp"
std::unordered_map<std::string, std::string> g_dict;

void Translate(const std::string& req, std::string* resp) 
{
     auto it = g_dict.find(req);
     if (it == g_dict.end()) 
     {
         *resp = "未找到";
         return;
     }
 *resp = it->second;
 return;
}

int main(int argc, char* argv[])
{
     if (argc != 3) 
     {
         printf("Usage ./dict_server [ip] [port]\n");
         return 1;
     }

  // 1. 初始化词典
     g_dict.insert(std::make_pair("hello", "你好"));
     g_dict.insert(std::make_pair("world", "世界"));
     g_dict.insert(std::make_pair("bit", "贼NB"));

  // 2. 启动服务器
     TcpServer server(argv[1], atoi(argv[2]));
     server.Start(Translate);
     return 0;
}

4.5、TCP通用客户端

tcp_client.hpp

#pragma once
#include "tcp_socket.hpp"
class TcpClient 
{
public:
     TcpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
     {
      // [注意!!] 需要先创建好 socket
         sock_.Socket();
     }

     ~TcpClient() 
     {
         sock_.Close();
     }

     bool Connect() 
     {
         return sock_.Connect(ip_, port_);
     }

     bool Recv(std::string* buf)
     {
         return sock_.Recv(buf);
     }

     bool Send(const std::string& buf)
     {
         return sock_.Send(buf);
     }
private:
     TcpSocket sock_;
     std::string ip_;
     uint16_t port_;
};

4.6、英译汉客户端

dict_client.cc

#include "tcp_client.hpp"
#include <iostream>
int main(int argc, char* argv[]) 
{
     if (argc != 3)
     {
         printf("Usage ./dict_client [ip] [port]\n");
         return 1;
     }
     TcpClient client(argv[1], atoi(argv[2]));
     bool ret = client.Connect();
     if (!ret) 
     {
         return 1;
     }    
     for (;;) 
     {
         std::cout << "请输入要查询的单词:" << std::endl;
         std::string word;
         std::cin >> word;
         if (!std::cin) 
         {
             break;
         }
         client.Send(word);
         std::string result;
         client.Recv(&result);
         std::cout << result << std::endl;
     }
     return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配;

注意:

  • 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动 多个客户端, 就会出现端口号被占用导致不能正确建立连接;
  • 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动 服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦;

4.7、测试多个连接的情况

再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信;

分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接 受新的请求;

当前的这个TCP, 只能处理一个连接, 这是不科学的;

5、简单的TCP网络程序(多进程版本)

通过每个请求, 创建子进程的方式来支持多连接;

tcp_process_server.hpp

#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer {
public:
     TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
     {
      // 需要处理子进程
         signal(SIGCHLD, SIG_IGN);
     }

     void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port,Handler handler)
     {
         int ret = fork();
         if (ret > 0) 
         {
          // father
          // 父进程不需要做额外的操作, 直接返回即可. 
          // 思考, 这里能否使用 wait 进行进程等待?
          // 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
          // [注意!!] 父进程需要关闭 new_sock
             new_sock.Close();
             return;
         } 
         else if (ret == 0) 
         {
          // child
          // 处理具体的连接过程. 每个连接一个子进程
             for (;;) 
             {
                 std::string req;
                 bool ret = new_sock.Recv(&req);
                 if (!ret) 
                 {
                  // 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完                
                  // 成了
                     printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
                     exit(0);
                 }
                 std::string resp;
                 handler(req, &resp);
                 new_sock.Send(resp);
                 printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port, 
                 req.c_str(), resp.c_str());
             }
         } 
         else 
         {
             perror("fork");
         }
     }

     bool Start(Handler handler) 
     {
      // 1. 创建 socket;
         CHECK_RET(listen_sock_.Socket());
      // 2. 绑定端口号
         CHECK_RET(listen_sock_.Bind(ip_, port_));
      // 3. 进行监听
         CHECK_RET(listen_sock_.Listen(5));
      // 4. 进入事件循环
         for (;;) 
         {
          // 5. 进行 accept
             TcpSocket new_sock;
             std::string ip;
             uint16_t port = 0;
             if (!listen_sock_.Accept(&new_sock, &ip, &port)) 
             {
                 continue;
             }
             printf("[client %s:%d] connect!\n", ip.c_str(), port);
             ProcessConnect(new_sock, ip, port, handler);
         }
         return true;
     }

private:
     TcpSocket listen_sock_;
     std::string ip_;
     uint64_t port_;
};

dict_server.cc 稍加修改

将 TcpServer 类改成 TcpProcessServer 类即可;

6、简单的TCP网络程序(多线程版本)

通过每个请求, 创建一个线程的方式来支持多连接;

tcp_thread_server.hpp

#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
typedef std::function<void (const std::string&, std::string*)> Handler;
struct ThreadArg 
{
     TcpSocket new_sock;
     std::string ip;
     uint16_t port;
     Handler handler;
};

class TcpThreadServer 
{
public:
     TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
     {}

     bool Start(Handler handler) 
     {
      // 1. 创建 socket;
         CHECK_RET(listen_sock_.Socket());
      // 2. 绑定端口号
         CHECK_RET(listen_sock_.Bind(ip_, port_));
      // 3. 进行监听
         CHECK_RET(listen_sock_.Listen(5));
      // 4. 进入循环
         for (;;) 
         {
          // 5. 进行 accept
             ThreadArg* arg = new ThreadArg();
             arg->handler = handler;
             bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
             if (!ret) 
             {
                 continue;
             }
             printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
          // 6. 创建新的线程完成具体操作
             pthread_t tid;
             pthread_create(&tid, NULL, ThreadEntry, arg);
             pthread_detach(tid);
         }
         return true;
     }

  // 这里的成员函数为啥非得是 static?
     static void* ThreadEntry(void* arg)
     {
      // C++ 的四种类型转换都是什么?
         ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
         ProcessConnect(p);
      // 一定要记得释放内存!!! 也要记得关闭文件描述符
         p->new_sock.Close();
         delete p;
         return NULL;
     }

  // 处理单次连接. 这个函数也得是 static 
     static void ProcessConnect(ThreadArg* arg) 
     {
      // 1. 循环进行读写
         for (;;) 
         {
             std::string req;
          // 2. 读取请求
             bool ret = arg->new_sock.Recv(&req);
             if (!ret)
             {
                 printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
                 break;
             }
             std::string resp;
          // 3. 根据请求计算响应
             arg->handler(req, &resp);
          // 4. 发送响应
             arg->new_sock.Send(resp);
             printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),
             arg->port, req.c_str(), resp.c_str());
     }
 }
private:
     TcpSocket listen_sock_;
     std::string ip_;
     uint16_t port_;
};

7、线程池版本的 TCP 服务器

修改服务器为线程池版本即可;

8、TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器初始化:

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为三次握手

数据传输的过程:

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  • 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答;
  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段;(第一次)
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0;(第二次)
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN;(第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器;(第四次)

这个断开连接的过程, 通常称为 四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些 段,再比如read()返回0就表明收到了FIN段;

9、TCP 和 UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/478601.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

kafka面试篇

消息队列的作用&#xff1a;异步、削峰填谷、解耦 高可用&#xff0c;几乎所有相关的开源软件都支持&#xff0c;满足大多数的应用场景&#xff0c;尤其是大数据和流计算领域&#xff0c; kafka高效&#xff0c;可伸缩&#xff0c;消息持久化。支持分区、副本和容错。 对批处理…

使用ansible剧本进行lvm分盘

使用 Ansible 剧本&#xff08;Playbook&#xff09;进行 LVM 分区管理是一种自动化的方式&#xff0c;可以帮助管理员在多台主机上批量管理逻辑卷。 部署环境 3台主机&#xff0c;添加硬盘 ansible-galaxy collection install community.general 联网执行&#xff0c;下…

蓝桥杯算法心得——游戏(优先队列)

大家好&#xff0c;我是晴天学长&#xff0c;优先队列的题&#xff0c;式子化简非常重要&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1) .游戏 2) .算法思路 附近最小 1.接收数据 2.找出最小的&#…

docker 配置国内阿里镜像源

在/etc/docker/目录下新建daemon.json文件 在文件中写入 {"registry-mirrors": ["https://jmphwhtw.mirror.aliyuncs.com"] } 以管理员身份运行命令 systemctl daemon-reload systemctl restart docker

羊大师羊奶靠谱么?消费口碑深度讲解

羊大师羊奶靠谱么&#xff1f;消费口碑深度讲解 羊大师羊奶&#xff0c;作为羊奶市场的一颗璀璨明星&#xff0c;其靠谱性一直备受广大消费者的关注。那么&#xff0c;羊大师羊奶究竟靠不靠谱呢&#xff1f;这需要我们从品牌实力与消费口碑两个方面进行深度解析。 从品牌实力来…

小米相册提取表格选项消失解决方法

小米相册这次的提取表格选项消失 故障原因&#xff1a; 因为部分用户将小爱视觉&#xff08;原名扫一扫&#xff09;这个应用给卸载了导致 解决方法 应用商店下载 小爱视觉 安装后授权即可使用 注意&#xff1a;系统最好为最新的 Xiaomi HyperOS系统

PFXA401SF Millmate控制器400设计用于提供大量功能 Brown Boveri BBC

PFXA401 Millmate控制器400设计用于提供大量功能&#xff0c;同时具有高度的用户友好性。 控制单元涵盖了所有边缘传感器的安装可能性。这意味着用户只需按照逐步说明设置控制单元&#xff0c;并计算正确的料带边缘和中心位置及宽度。 计算相对于轧机的真实带材位置和宽度 过滤…

RabbitMQ在Java中使用 SpringBoot 从基础到高级

充分利用每一个监听者 需要充分利用每一个消费者&#xff0c;需要在配置文件中加上prefetch配置并设置为1 rabbitmq:listener:simple:prefetch: 1 # 每次只能获取一条消息&#xff0c;处理完成才能获取下一个消息创建交换机和队列 创建队列 "fanout.queue1"&…

云农场种植、领养、收获,认养模式新浪潮

​ 小编介绍&#xff1a;10年专注商业模式设计及软件开发&#xff0c;擅长企业生态商业模式&#xff0c;商业零售会员增长裂变模式策划、商业闭环模式设计及方案落地&#xff1b;扶持10余个电商平台做到营收过千万&#xff0c;数百个平台达到百万会员&#xff0c;欢迎咨询。 在…

服务器中了mallox勒索病毒还能恢复数据吗?

什么是mallox勒索病毒&#xff1f; mallox是一种最近多发的勒索病毒类型&#xff0c;它主要针对企业的Web应用和数据库服务器进行攻击。mallox后缀的勒索病毒会加密用户的重要文件数据并以此为要挟索要赎金。该类病毒会绕过企业的防火墙和各种防护软件&#xff0c;对目标设备进…

Python 将HTML转为PDF、图片、XML、XPS格式

网页内容是信息传播的主要形式之一。在Web开发中&#xff0c;有时候我们需要将HTML文件以不同的格式保存或分享&#xff0c;比如PDF、图片&#xff08;如PNG或JPEG&#xff09;、XML或XPS等。这些格式各有优势&#xff0c;适合不同的用途。在这篇文章中&#xff0c;我们将介绍如…

GPU 使用率监测及可视化

1. 使用 nvidia-smi可视化 直接在终端输入nvidia-smi动态查看GPU的使用情况watch -n 0.5 nvidia-smi其中0.5表示每隔0.5秒更新一次,时间可以调整 2. 使用nvitop可视化 2.1 nvitop的使用 (1) 安装 pip install nvitop(2) 查看GPU使用率 nvitop

【LAMMPS学习】三、构建LAMMPS(11)开发构建选项

3. 构建 LAMMPS 3.11.开发构建选项 LAMMPS 中的构建过程提供了一些在开发、测试或调试期间有用的额外选项。 3.11.1.监视编译标志&#xff08;仅限 CMake&#xff09; 有时需要验证 CMake 构建生成的编译标志的完整序列。要在编译期间启用更详细的输出&#xff0c;可以使用…

DenseNet《Densely Connected Convolutional Networks》

DenseNet学习笔记 摘要引言DenseNetsResNetsDense connectivityResNet 和 DenseNet 的对比DenseBlock 向前传播的过程Composite functionPooling layersGrowth rateBottleneck layersCompression 压缩实现细节 实验训练 代码复现 摘要 最近的研究表明&#xff0c;如果卷积网络…

鸿蒙Harmony应用开发—ArkTS-全局UI方法(自定义组件的生命周期)

自定义组件的生命周期回调函数用于通知用户该自定义组件的生命周期&#xff0c;这些回调函数是私有的&#xff0c;在运行时由开发框架在特定的时间进行调用&#xff0c;不能从应用程序中手动调用这些回调函数。 说明&#xff1a; 本模块首批接口从API version 7开始支持&#x…

c语言(动态内存管理函数)

1. 为什么要有动态内存分配 我们已经掌握的内存开辟⽅式有&#xff1a; int arr[10] {0}; char a; 但是上述的开辟空间的⽅式有两个特点&#xff1a; 但是上述的开辟空间的⽅式有两个特点&#xff1a; • 空间开辟⼤⼩是固定的。 • 数组在申明的时候&#xff0c;必须指…

php 对接Mintegral汇量海外广告平台收益接口Reporting API

今天对接的是Mintegral广告reporting api接口&#xff0c;拉取广告收益回来自己做统计。记录分享给大家 首先是文档地址,进入到Mintegral后台就能看到文档地址以及参数&#xff1a; 文档地址&#xff1a;https://cdn-adn-https.rayjump.com/cdn-adn/reporting_api/MintegralRA.…

Java基于微信小程序的二手交易系统的实现(V2.0)

博主介绍&#xff1a;✌Java徐师兄、7年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、Python 技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#…

C++初阶:string类相关练习题

目录 1. 字符串相加2. 反转字母3. 字符串中唯一字母4. 字符串中最后一个单词5. 验证回文串6. 反转字符II7. 反转字符串中的单词8. 字符串相乘 1. 字符串相加 题目信息&#xff1a; 题目连接&#xff1a; 字符串相加 class Solution { public:string addStrings(string num1, s…

Ubuntu24.04即将发布,支持Linux内核6.8。该版本会有哪些亮点?

更多内容在 Canonical 计划 2024 年 4 月发布 Ubuntu 24.04 LTS 发行版&#xff0c;将获得长达 12 年的支持。Ubuntu 24.04 LTS&#xff0c;代号“Noble Numbat”&#xff0c;是Ubuntu发行版的最新长期支持&#xff08;LTS&#xff09;版本。12年的支持意味着你会得到 五年的维…