网络编程 - 套接字

news2024/11/14 14:01:47

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.coloradmin.cn/o/1537168.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows server 2012 R2系统怎么显示桌面图标

当我们在使用Windows server2012 R2服务器计算机时&#xff0c;为了方便&#xff0c;我们可以添加桌面图标。下面就给大家分享一下添加桌面图标的方法&#xff1b; 操作步骤如下&#xff1a; 1、第一步&#xff0c;我们打开server服务器&#xff0c;就可以看到如下画面&#x…

Elasticsearch从入门到精通-06ES统计分析语法

Elasticsearch从入门到精通-06ES统计分析语法 bucket和metric概念简介 bucket就是一个聚合搜索时的数据分组。如&#xff1a;销售部门有员工张三和李四&#xff0c;开发部门有员工王五和赵六。那么根据部门分组聚合得到结果就是两个bucket。销售部门bucket中有张三和李四&…

【ESP32 Arduino】定时器的使用

文章目录 前言一、ESPTIMER定时器的介绍1.1 定时器是什么 二、分频系数2.1 为什么需要分频系数2.2 分频系数怎么计算 三、定时器的使用3.1 初始化定时器对象3.2 设置中断时间3.3 设置回调函数3.4 使能定时器 四、示例代码总结 前言 在嵌入式系统中&#xff0c;定时器是一项重要…

Iterator对象功能学习

package config;import java.util.Iterator; import java.util.Properties; import java.util.Set;/*** 这个类演示了如何使用Properties类来存储和访问键值对。* Properties类继承自Hashtable&#xff0c;因此它可以用来存储键值对数据&#xff0c;且支持同步。*/ public clas…

Git多分支管理实践

想要实现本地文件对远程文件的管理&#xff0c;必须懂得Git的相关操作。 工作中不免会遇到一个仓库多个分支的管理。 git多分支管理属于git的进阶版操作&#xff0c;下面我们来看看。 1. 拉取一个git仓库 git仓库名假设为&#xff1a;test_demo&#xff0c;默认是主仓库&…

文件路径引用错误

报错This dependency was not found: * /view/superAdmin/menu/icon.vue in ./node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/vue-loader/lib??vue-loader-opti…

云手机为企业出海提供多元化解决方案

随着全球市场的不断扩大&#xff0c;发展出海业务已经成为许多企业开发新市场的重要途径。而在这个竞争激烈的市场中&#xff0c;云手机作为一种全新的工具&#xff0c;为企业提供了多元的解决方案&#xff0c;助力其在海外市场通过各种方式取得成功。本文将介绍企业可以通过云…

利用pexpect实现ssh自动登录时命令行无法自动换行问题解决

问题描述 使用python的pexpect模块的pexpect.spawn()进行ssh自动登录时&#xff0c;出现超出一定长度&#xff08;80个字符&#xff09;时光标自动切换到本行行首进行覆盖输入的情形 原因 使用spawn时输入窗口大小默认限制为[24,80]&#xff08;可通过spawn类的getwinsize(…

Maximizing Influence with Graph Neural Networks

Abstract 寻找使网络影响力最大化的种子集是一个众所周知的 NP 难题。尽管贪心算法可以提供接近最优的解决方案&#xff0c;但影响估计的子问题导致解决方案效率低下。在这项工作中&#xff0c;我们提出了 GLIE&#xff0c;这是一种图神经网络&#xff0c;它学习如何估计独立级…

新能源汽车充电桩消防安全视频智能可视化监管建设方案

一、方案背景 据应急管理部门统计公布的数据显示&#xff0c;仅2023年第一季度&#xff0c;新能源汽车自燃率就上涨了32%&#xff0c;平均每天就有8辆新能源汽车发生火灾&#xff08;含自燃&#xff09;。在已查明起火原因中&#xff0c;58%源于电池问题&#xff0c;19%源于碰…

备战蓝桥杯Day34 - 每日一题

题目描述 解题思路 1.输入数据n&#xff0c;并将字符串类型转换成整数类型 2.求出输入n是2的几次幂&#xff08;调用math库中的求对数的方法&#xff09;&#xff0c;在下面的循环中要用到 3.定义sum和&#xff0c;将抽取到的牌的总数加起来存储 4.count 0 # 记录 2 的第几…

分享一下现在哪些相亲软件好用?盘点四款热门的相亲软件

很多单身的小伙伴都在问有哪些靠谱的相亲软件&#xff0c;平时自己的工作和生活圈子太小没有办法找到结婚对象&#xff0c;通过手机上的相软件找对象也是一个不错的尝试。今天我来为大家分享几款成功率比较高的婚恋相亲软件&#xff01; 1、一伴婚恋 这款APP目前是线上找对象…

DP:斐波那契数列模型

创作不易&#xff0c;感谢三连支持 &#xff01; 斐波那契数列用于一维探索的单峰函数之中&#xff0c;用于求解最优值的方法。其主要优势为&#xff0c;在第一次迭代的时候求解两个函数值&#xff0c;之后每次迭代只需求解一次 。 一、第N个泰波那契数 . - 力扣&#xff08;…

JAVA 栈和队列总结

除了最底层下面三个是实现类&#xff0c;其他都是接口。 双端队列&#xff08;队头队尾都可以插入和删除元素&#xff09;的方法&#xff1a; 普通队列方法&#xff1a; 常用的是add(),poll(), element() 我们用Deque(双端队列)实现栈 Deque当栈用的时候的方法。 deque.push…

【史上最全面arduino esp32教程】SPI层次结构SPI协议与SPI控制器结构

文章目录 前言一、SPI 程序层次1.1 硬件原理图1.2 硬件框图1.3 软件层次 二、SPI协议2.1 硬件连线2.2 如何访问SPI设备2.3 SPI 框图 总结 前言 欢迎阅读本篇文章&#xff0c;将为您介绍Arduino ESP32上的SPI通信协议。SPI&#xff08;Serial Peripheral Interface&#xff09;…

设计数据库之内部模式:SQL基本操作

Chapter4&#xff1a;设计数据库之内部模式&#xff1a;SQL基本操作 笔记来源&#xff1a; 1.《漫画数据库》—科学出版社 2.SQL | DDL, DQL, DML, DCL and TCL Commands 设计数据库的步骤&#xff1a; 概念模式 概念模式(conceptual schema)是指将现实世界模型化的阶段进而&…

AI换脸软件rope最新更新的蓝宝石中文版下载

rope换脸软件蓝宝石版下载地址&#xff1a;点击下载 最近AI软件非常的火爆&#xff0c;今天就给大家带来一个可以AI替换人脸的工具rope&#xff0c;得益于机器学习技术的不断发展&#xff0c;rope经过深度神经网络的无数次迭代优化&#xff0c;最终得出的模型可以自动学习和识…

Oh My Bug || PHPmyAdmin导入csv文件时,502报错

解决&#xff1a; 在宝塔面板文件配置中加入一下代码 location / { proxy_pass http://localhost:888; } location /backend-api { rewrite ^/backend-api(.*)$ $1 break; proxy_pass http://你的ip地址; }

elasticsearch数据聚合

数据聚合 聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如: 什么品牌的手机最受欢迎? 这些手机的平均价格、最高价格、最低价格? 这些手机每月的销售情况如何? 实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实…

java 泛型(中)

本篇文章主要说明的是泛型类、泛型接口、泛型方法等。 在学习之前&#xff0c;希望能对泛型有个大概了解&#xff0c;可参考链接 java 泛型&#xff08;上&#xff09;-CSDN博客 1、泛型类 &#xff08;1&#xff09;格式&#xff1a;修饰符 class 类名<类型>{} &…