当前位置: 首页 > news >正文

Linux网络编程:使用UDP和TCP协议实现网络通信

目录

一. 端口号的概念

二. 对于UDP和TCP协议的认识

三. 网络字节序

3.1 字节序的概念

3.2 网络通信中的字节序

3.3 本地地址格式和网络地址格式 

四. socket编程的常用函数 

4.1 sockaddr结构体

4.2 socket编程常见函数的功能和使用方法

五. UDP协议实现网络通信

5.1 UDP协议服务端的封装

5.2 UDP协议客户端的封装

六. TCP协议实现网络通信

6.1 TCP协议服务端的封装

6.2 TCP协议客户端的封装

七. 总结


一. 端口号的概念

进行网络通信,其根本目的并不是让两个主机之间进行通信,而是让运行在主机上的两个进程之间相互通信。如,我们在日常生活中经常要通过微信发送消息,我发送的消息必须要经过网络传输,才能够被对方接受。我发送的消息,并没有被对方主机的其他应用接受,而只是被对方主机上运行的微信这一进程接受。由此可见,网络通信本质上是网络中的两主机通过网络实现进程间通信,为此,OS必须通过特定的方式,来标识接受数据的进程

端口号,是在某一主机上,用来标识进程唯一性的编号,与之对应的IP地址,用于表示网络中唯一的一台主机,因此,IP地址 + 端口号,可用于表示全网中唯一的一个进程

关于端口号,有如下的基本结论:

  • 端口号用于表示主机中的唯一一个进程。
  • 端口号是一个16位、2字节的整数,其数据类型为uint16_t。
  • IP + 端口号,可用于表示全网唯一一个进程。
  • 一个端口号只能对应一个进程,而一个进程能够对应多个端口号。

端口号(port)和进程pid之间的关系:每个进程都有对应的pid,用于在系统中标识特定进程,但不进行网络通信的进程不需要有端口号,理论上讲id + port,也可以识别网络中唯一一个进程,但是使用端口号和id,能够实现系统进程和网络通信功能之间的解耦。

二. 对于UDP和TCP协议的认识

UDP协议,即用户数据报协议(User Datagram Protocol),其特征有:

  • 属于传输层面的协议。
  • 不需要连接 -- 一方给另一方发送数据时,不需要另一方处于等待状态。
  • 不可靠传输 -- 可能出现失帧、丢包等问题。
  • 面向数据报的通信方式。

TCP协议,即传输控制协议(Transmission Control Protocol),其特征有:

  • 属于控制层面的协议。
  • 需要连接 -- 一方给另一方发送数据时,另一方必须处于等待状态,发送数据前要预先建立连接才能够发送成功。
  • 可靠传输 -- 不会出现失帧、丢包等问题。
  • 面向字节流的通信。

对于需要连接和不需要连接的理解:需要链接,类似于生活中的接打电话,我们给一方打电话的时候,对方需要听到电话铃声,确认接听才能通信,确认接听电话,就类似于网络通信中的建立链接。不需要连接,类似于生活中发送电子邮件的通信方式,我们要给某人发送电子邮件时,可以不用事先通知对方,只要发送,等待对方合适的时候查看即可,对方不需要事先准备接收邮件,即不需要连接。

对于可靠传输和不可靠传输的理解:可靠传输和不可靠传输并不是好坏的评判标准,原因是:(a). UDP协议虽然可能存在丢包失帧等问题,但是发生问题的概率极小,有些时候这并不是不可以接受的。 (b). 虽然TCP协议不会出现UDP这样的不可靠传输的问题,但是可靠通信的建立,是需要成本的,在有些可以一定程度接受数据传输出现问题的场景,采用TCP协议综合效益并不高。

三. 网络字节序

3.1 字节序的概念

内存中存储数据的字节序有两种:(1). 小端字节序 -- 低位存储在低地址,高位存储在高地址。(2). 大端字节序 -- 低位存储在高地址,高位存储在低地址。

图3.1以十六进制表示的数据int num = 0XAABBCCDD为例,展示了大端机和小端机存储数据的规则,这个数据第低位为DD,高位为AA。

图3.1 小端机和大端机使用内存的方式

3.2 网络通信中的字节序

假设这样一种场景,一台小端机要通过网络给一台大端机发送数据,假设他们以他们各自的字节序发送向网络中发数据和从网络中读数据,那么就会出现“乱序”问题,因此需要一定的协议,用于规范网络数据的字节序,以避免“乱序问题”。

规定:网络中的数据,全部采用大端字节序。

我们有时候无法确定发送的数据,或者从网络中读取来的数据是大端还是小端,为了保证发送和读取数据的可靠性,C标准库提供了下面4个函数,可以实现网络和主机数据之间的相互转换:

  • uint32_t htonl(uint32_t hostlong) -- 将32位主机数据转为网络格式。
  • uint16_t htons(uint16_t hostshort) -- 将16位主机数据转为网络格式。
  • uint32_t ntohl(uint32_t netlong) -- 将32位网络数据转为主机格式。
  • uint16_t ntohs(uint16_t netshort) -- 将16位网络数据转为主机格式。 

3.3 本地地址格式和网络地址格式 

一般我们在主机中标识ip地址,都采用const char*数据类型、点分十进制方法来表示, 如1.23.122.234,但是在网络中,为了节省资源,ip应当采用四字节的方法来表示,下面几个函数的功能,是实现本地const char*点分十进制ip格式和网络四字节ip格式之间的转化:

  • in_addr_t inet_addr(const char *cp) -- 本地ip格式转网络ip格式。
  • const char* inet_ntoa(struct in_addr in) -- 网络ip格式转本地ip格式。
  • const char *inet_ntop(int af, const void *src, char *dfs, socklen_t len) -- 网络ip格式转本地ip格式,将转换后的结果存储在dfs所指向的空间中。

注意:一般建议使用inet_ntop函数,而不是采用inet_ntoa函数,因为inet_ntoa为了返回本地格式的ip,会在函数内部开辟一块static空间来记录转换来的结果,这样就带来两个问题:1. 线程不安全  2. 如果多次调用inet_ntoa函数,那么最后一次调用的返回结果会覆盖掉前面的结果。而采用inet_ntop函数,返回结果会被存储在用户指定的buffer空间中,杜绝了inet_ntoa函数的这两个问题。

inet_ntop函数的af参数表示通信方式(AF_INET表示ipv4格式网络地址,AF_INET6表示ipv6格式网络地址),src为指向struct sin_addr类型数据的指针,dfs为接收结果的输出型参数。

四. socket编程的常用函数 

4.1 sockaddr结构体

图4.1给出了sockaddr、sockaddr_in和sockaddr_un的结构,其中sockaddr为socket API抽象出来的一种结构体,使用与ipv4、ipv6、udp、tcp、本地通信,等各种形式的socket。

sockaddr_in为网络通信使用的结构体,sockaddr_un为本地通信使用的结构体。

图4.1 sockaddr结构体

网络通信常用的结构为sockaddr_in,其内容包括:16位地址类型AF_INIT,用于确定通信方式为网络通信、32位IP地址用于在网络中定位特定的主机、8字节填充没有实际意义,一般为0。

struct sockaddr_in的定义:

struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port;			/* Port number.  */struct in_addr sin_addr;		/* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];};

struct in_addr的定义:

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

4.2 socket编程常见函数的功能和使用方法

对于socket程序,一般包含四个头文件,即可支持所有的socket相关函数:

  1. #include <sys/types.h> 
  2. #include <sys/socket.h> 
  3. #include <netinet/in.h> 
  4. #include <apra/inet.h>

创建socket文件描述符函数 -- socket:

  • UDP协议和TCP协议、客户端以及服务端,都需要创建socket文件描述符。
  • socket文件描述符与系统级文件描述符的本质相同,都是数组arrayfd对应的一个下标,表示进程打开了某个特定的文件。

socket函数 -- 创建socket文件描述符

函数原型:int socket(int domain, int type, int protocol)

函数参数:

  • domain -- 选取通信的范围(网络还是本地),AF_INET为网络通信,AF_LOCAL为本地通信。
  • type -- 通信类型,UDP协议传SOCK_DGRAM,TCP协议传SOCK_STREAM。
  • protocal -- 指定协议类型,一般传0即可。

返回值:如果成功,返回创建的socket文件描述符,如果失败返回-1。

绑定端口号-- bind:

  • 将用户给出和端口号,在内核中与当前的进程相关联。
  • 在TCP和UDP协议的服务端,都需要使用bind绑定。
  • 在客户端,不采用bind绑定用户设定的端口号,因为如果客户端由用户设置端口号,如果这个端口号被其他进程占用,那么会出现绑定失败的问题。
  • 客户端在第一次给服务端发送消息时,由OS自动分配端口号。

bind函数 -- 绑定端口号

函数原型:int bind(int socket, const struct sockaddr *address, socklen_t *len)

函数参数:

  • socket -- socket文件描述符
  • address -- 输入型参数,用于指定套接字sockaddr
  • len -- 输入型参数,用于给出sockaddr结构体的长度(占用多少字节)

返回值:成功返回0,失败返回-1。

设置监听状态 -- listen:

  • 一般用于TCP协议中的服务器端。
  • 在TCP协议中,只有设置服务器处于监听状态,客户端才可以与服务器建立链接并实现通信。

listen函数 -- 设置监听状态

函数原型:int listen(int socket, int backlog)

函数参数:

  • socket -- socket文件描述符
  • backlog -- 应传一个不太大也不太小的数字。

返回值:成功返回0,失败返回-1。

建立通信连接 -- connect:

  • 一般用于TCP协议的客户端。
  • 在TCP协议中,如果客户端希望与服务端通信,那么就必须与服务端建立连接。
  • 客户端使用connect函数与服务器建立链接,必须保证服务器处于监听状态。

connect函数 -- 建立通信连接

函数原型:int connect(int socket, struct sockaddr *address, socklen_t len)

函数参数:

  • socket -- socket文件描述符
  • address -- 被链接的一端的套接字
  • len -- 套接字长度

返回值:成功返回0,失败返回-1。

接受通信另一方的连接请求 -- accept:

  • 如果另一方尝试与本地进程建立网络通信,那么accept可以用于接收对方的连接。
  • accept函数一般用于TCP协议的服务端,只有accept函数成功接收了另一端的连接请求,才算是真正建立起来通信连接。

accept函数 -- 接受通信另一端的连接请求

函数原型:int accept(int socket, struct sockaddr_in *address, socklen_t *len)

函数参数:

  • socket -- socket文件描述符。
  • address -- 输出型参数,用于获取请求连接的一方的套接字信息。
  • len -- 套接字长度。

返回值:成功返回用于通信的“网络文件”的文件描述符,失败返回-1。

从网络中读取数据函数 -- recvfrom:

  • 从网络中读取指定长度的数据,到指定的缓冲区中。
  • 用于UDP协议的客户端和服务端读取对端消息。

recvfrom函数 -- UDP协议从网络中读取数据

函数原型:ssize_t recvfrom(int socket, void *buffer, size_t length, int flag, struct sockaddr *addr, socklen_t *addr_length)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 接受数据的缓冲区。
  • length -- 至多读取的字节数。
  • flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。
  • addr -- 输出型参数,用于接受发送数据的主机ip及进程端口号。
  • addr_length:套接字长度。

返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。

从网络中读取数据 -- recv:

  • 从网络中读取数据到指定缓冲区。
  • 用于TCP协议中客户端和服务端读取对端消息。

recvfrom函数 -- TCP协议从网络中读取数据

函数原型:ssize_t recv(int socket, void *buffer, size_t length, int flag)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 接受数据的缓冲区。
  • length -- 至多读取的字节数。
  • flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。

返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。

向网络中发送数据函数 -- sendto:

  • 可以通过指定ip和端口号,将数据发送给指定主机的某个进程。
  • 给定端口号和ip时要注意将ip和端口号转为网络格式。

sendto函数 -- 向网络中发送数据

函数原型:ssize_t sendto(int socket, void *buffer, size_t length, int flag, const struct sockaddr *addr, socklen_t addr_length)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 存储待发送数据的缓冲区。
  • length -- 要发送的数据的字节数。
  • flag -- 发生数据的方式,一般直接设置为0即可。
  • addr -- 指定接受数据的主机ip和端口号。
  • addr_length:套接字长度。

返回值:成功返回发送出去的字节数,失败返回-1。

五. UDP协议实现网络通信

5.1 UDP协议服务端的封装

本文实现一个服务端的demo程序,其功能为:服务端从客户端读取数据,记录数据源主机的ip和端口号,如果源主机第一次向服务器发送数据,就将该主机的ip和端口号插入到哈希表中,每次服务器接受到数据,就将数据发回哈希表中记录的主机,这样就模拟实现了简单的群聊功能。

服务端初始化步骤:创建socket文件描述符 -> 绑定端口号。

服务端启动后的工作流程:通过recvfrom函数从客户端读取数据 -> 判断发送数据的客户端是否已经向服务器发送过数据,如果没有,那么将客户端的ip和端口号插入哈希表 -> 将读取到的数据发回哈希表中记录的客户端。

启动服务端程序时,要指定端口号,以便客户端能够与服务端建立通信,一般来说服务端不用显示设定ip,而是通过INADDR_ANY来设置,这表示无论客户端ip是多少,都可以实现与服务端的通信。

Log.hpp文件(日志打印相关内容):

#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>#define DEBUG  0
#define NORMAL 1
#define WARING 2
#define ERROR  3
#define FATAL  4const char* g_levelMap[5] = 
{"DEBUG","NORMAL","WARING","ERROR","FATAL"
};void logMessage(int level, const char *format, ...)
{// 1. 输出常规部分time_t timeStamp = time(nullptr);struct tm *localTime = localtime(&timeStamp);printf("[%s]  %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);// 2. 输出用户自定义部分va_list args;va_start(args, format);vprintf(format, args);va_end(args);
}

udp_serve.hpp文件(对服务端封装):

#pragma once#include "Log.hpp"
#include <iostream>
#include <string>
#include <unordered_map>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>class Server
{
public:// 服务端构造函数Server(uint16_t port, const std::string& ip = ""): _port(port), _ip(ip), _sock(-1){ }// 初始化服务器void init(){// 1. 创建网络套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);   if(_sock < 0)  // 检查套接字创建成功与否{logMessage(FATAL, "%d:%s\n", errno, strerror(errno));exit(2);}logMessage(DEBUG, "套接字创建成功, _sock:%d\n", _sock);// 2. bind:将用户设置的ip和port在内核中与进程相关联struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// INADDR_ANY: 表示服务器在工作过程中可以从任意ip获取数据// inet_addr函数: 将主机ip转为4字节网络iplocal.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());   local.sin_port = htons(_port);   // 将主机端口转换为网络端口格式if(bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "%d:%s\n", errno, strerror(errno));exit(3);}logMessage(DEBUG, "用户设置的ip和port在内核中与进程相关联成功!\n");}// 启动服务器程序void start(){// 服务器进程永不退出// 从客户端读取数据char buffer[1024];    // 输出缓冲区char key[128];         // 存储客户端ip和端口号struct sockaddr_in sock_cli;             // 客户端套接字memset(&sock_cli, 0, sizeof(sock_cli));  // 初始化0socklen_t len = sizeof(sock_cli);        // 输入型参数 -- 套接字长度std::string addr_cli;   // 数据源客户端的ipuint16_t port_cli;      // 数据源客户端的端口号while(true){// 输出读取到的数据memset(buffer, 0, sizeof(buffer));memset(key, 0, sizeof(key));ssize_t n = recvfrom(_sock, (void *)buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&sock_cli, &len);if(n > 0){buffer[n] = 0;   // 添加尾部'/0'addr_cli = inet_ntoa(sock_cli.sin_addr);  // inet_ntoa函数负责将网络ip转换为主机ipport_cli = ntohs(sock_cli.sin_port);  // 网络套接字转换为主机套接字snprintf(key, 128, "[%s-%d]", addr_cli.c_str(), port_cli);printf("[%s:%d]# %s\n", addr_cli.c_str(), port_cli, buffer);  // 输出发送端的ip和port,以及发送的内容}else if(n == 0){logMessage(DEBUG, "未读取到数据!\n");continue;}else // 数据读取失败{logMessage(ERROR, "读取数据失败!\n");continue;}// 将客户端的ip和port插入到哈希表if(_mp.find(key) == _mp.end()){_mp.insert({key, sock_cli});logMessage(NORMAL, "成功插入客户端, %s\n", key);}// 将读取到的数据全部发送给客户端主机for(const auto& iter : _mp){std::string msg_cli;msg_cli += key;msg_cli += "# ";msg_cli += buffer;if(sendto(_sock, msg_cli.c_str(), msg_cli.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second)) < 0){logMessage(ERROR, "服务器发回消息失败!");continue;} logMessage(NORMAL, "向客户端写回数据 -- %s\n", iter.first.c_str());}}}// 析构函数~Server(){if(_sock >= 0){close(_sock);}}private:uint16_t _port;    // 端口号std::string _ip;   // 服务器ip地址int _sock;         // 套接字对应文件描述符std::unordered_map<std::string, struct sockaddr_in> _mp; // 哈希表,记录接收到信息的客户端的ip和port 
};

udpserve.cc文件(服务端源文件):

#include "udp_serve.hpp"
#include <memory>void usage(const char *command)
{std::cout << "\nUsage# " << command << " port\n" << std::endl; 
}int main(int argc, const char **argv)
{if(argc != 2){usage(argv[0]);exit(1);}uint16_t port = static_cast<uint16_t>(atoi(argv[1]));std::unique_ptr<Server> psvr(new Server(port));psvr->init();psvr->start();return 0;
}

5.2 UDP协议客户端的封装

本文采用多线程的方式实现UDP客户端demo程序,一个线程负责从服务器读取数据,另一个线程负责向服务器发送数据。

客户端初始化init:创建socket即可,不需要bind端口号,当客户端第一次向服务端发送数据的时候,OS会自动为客户端进程分配端口号。

客户端启动函数执行的工作:创建两个线程,一个调用recvfrom函数从服务端读数据,另一个调用sendto函数向服务器写数据。

启动客户端程序时,需要告知服务器对应的ip和端口号,才能够成功与服务器建立通信。

udp_client.hpp文件(封装客户端):

#pragma once#include "Log.hpp"
#include "Thread.hpp"
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>struct SendMessageData
{int _sock;struct sockaddr_in _sock_srv;socklen_t _len;SendMessageData(int sock, struct sockaddr_in sock_srv): _sock(sock), _sock_srv(sock_srv), _len(sizeof(sock_srv)){}
};class Client
{
public:// 构造函数Client(const std::string &ip, uint16_t port): _ip(ip), _port(port), _sock(-1){}// 初始化函数void init(){// 1. 创建网络套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}// 2. 绑定 -- 客户端需要绑定,但一般不会由程序员来进行绑定// 而是在第一次发送消息时,由OS自动分配ip和port}// 客户端启动程序void start(){// 创建服务器的struct sockaddrstruct sockaddr_in srv_sock;memset(&srv_sock, 0, sizeof(srv_sock));srv_sock.sin_family = AF_INET;srv_sock.sin_addr.s_addr = inet_addr(_ip.c_str()); // 主机ip转为网络ipsrv_sock.sin_port = htons(_port);                  // 主机port转为网络portSendMessageData sendData(_sock, srv_sock);// 发送消息thread th_send(send_message, (void *)&sendData);th_send.start();// 接受反馈回来的消息thread th_recieve(recieve_message, (void *)&_sock);th_recieve.start();th_send.join();th_recieve.join();}// 析构函数~Client(){if (_sock < 0){close(_sock);}}private:static void *send_message(void *args){SendMessageData *ptr = (SendMessageData *)args;while (true){std::string msg;std::cerr << "请输入你要发送的消息: " << std::flush;std::getline(std::cin, msg); // 按行读取sendto(ptr->_sock, msg.c_str(), msg.size(), 0, (const sockaddr *)&ptr->_sock_srv, ptr->_len);}return nullptr;}static void *recieve_message(void *args){// memset(buffer, 0, sizeof(buffer));char buffer[1024];while (true){struct sockaddr tmp;memset(&tmp, 0, sizeof(tmp));socklen_t len = sizeof(tmp);ssize_t n = recvfrom(*(int *)args, (void *)buffer, sizeof(buffer) - 1, 0, (sockaddr *)&tmp, &len);if (n > 0){// std::cerr << "aaaa" << std::endl;buffer[n] = '\0';printf("%s\n", buffer);}}return nullptr;}std::string _ip; // 发生数据的主机ipuint16_t _port;  // 端口号int _sock;       // 套接字
};

udp_client.cc文件(客户端源文件):

#include "udp_client.hpp"
#include <memory>
#include <signal.h>void Usage(const char *command)
{std::cout << "/nUsage: " << command << " ip port\n" << std::endl; 
}int main(int argc, const char **argv)
{if(argc != 3){Usage(argv[0]);exit(1);}// signal(SIGCHLD, SIG_IGN);std::unique_ptr<Client> pcli(new Client(argv[1], atoi(argv[2])));pcli->init();pcli->start();return 0;
}

六. TCP协议实现网络通信

6.1 TCP协议服务端的封装

本文采用多进程的方式来编写服务端demo代码,每次接收到客户端的连接请求,就为这个客户端创建一个进程,用于与该客户端通信。

服务端初始化init:创建socket套接字 -> 将本地ip和端口号在内核中与当前进程绑定 -> 设置服务端进程处于listen状态,以便随时接受客户端的连接请求。

服务端启动start:接受客户端的连接请求并记录请求连接的客户端的套接字 -> 创建子进程 -> 在子进程中调用读取客户端发送的信息 -> 读取成功后,发回给客户端。

启动服务器时,需要显示给出端口号,以便客户端能够顺利连接到服务器。

tcp_server.hpp文件(服务端封装):

#pragma once#include "Log.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>const int g_size = 1024;static void server(int serverSock, struct sockaddr_in sock_cli)
{char buffer[g_size];  // 存储读取数据的缓冲区while(true){ssize_t n = recv(serverSock, buffer, g_size - 1, 0);   // 读取数据if(n > 0){buffer[n] = '\0';uint16_t cli_port = ntohs(sock_cli.sin_port);char cli_addr[20]; // 地址memset(cli_addr, 0, sizeof(cli_addr));// socklen_t len = sizeof(sock_cli.sin_addr);inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, sizeof(cli_addr));printf("[%s-%d] %s\n", cli_addr, cli_port, buffer);// 发回客户端send(serverSock, buffer, strlen(buffer), 0);}else if(n == 0){logMessage(DEBUG, "对端关闭,读取结束!\n");break;}else  // n < 0{logMessage(ERROR, "读取失败!\n");break;}}close(serverSock);
}class TcpServer
{
public:TcpServer(uint16_t port, const std::string &ip = ""): _port(port), _ip(ip), _listenSock(-1){ }// 服务器初始化void init(){// 1. 创建网络套接字_listenSock = socket(AF_INET, SOCK_STREAM, 0);if (_listenSock < 0) // 创建socket失败{logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));exit(2);}logMessage(NORMAL, "socket success, _listenSock:%d\n", _listenSock);// 2. 绑定端口号struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;                                                // 设置网络协议族local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 本地格式地址转为网络格式local.sin_port = htons(_port);                                             // 本地格式端口号转为网络格式if (bind(_listenSock, (const sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s\n", errno, strerror(errno));exit(3);}logMessage(NORMAL, "bind success, %s:%d\n", _ip.c_str(), _port);// 3. 设置监听状态if (listen(_listenSock, _backlog) < 0){logMessage(FATAL, "listen error, %d:%s\n", errno, strerror(errno));exit(4);}logMessage(NORMAL, "listen success\n");logMessage(NORMAL, "Server Init Success!\n");}// 运行服务器void start(){while (true){// 接受客户端的链接请求struct sockaddr_in sock_cli;memset(&sock_cli, 0, sizeof(sock_cli));socklen_t len = sizeof(sock_cli);int serverSock = accept(_listenSock, (struct sockaddr *)&sock_cli, &len);if (serverSock < 0) // 接受客户端请求失败{logMessage(ERROR, "accept error, %d:%s\n", errno, strerror(errno));continue;}uint16_t cli_port = ntohs(sock_cli.sin_port);char cli_addr[20]; // 地址memset(cli_addr, 0, sizeof(cli_addr));inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, len);cli_addr[strlen(cli_addr)] = '\0';logMessage(NORMAL, "accept success [%s-%d]\n", cli_addr, cli_port);// 多进程接受客户端信息pid_t id = fork();if (id == 0){if (fork() > 0)exit(0); // 子进程退出// 子进程的子进程(孙子进程)此时变为孤儿进程// 由1号进程领养,OS自动回收进程server(serverSock, sock_cli);exit(0);}waitpid(id, nullptr, 0);close(serverSock);}}// 析构函数~TcpServer(){if (_listenSock >= 0){close(_listenSock);}}private:uint16_t _port;  // 端口号std::string _ip; // 本地ipint _listenSock; // socket文件描述符static const int _backlog = 20;
};

tcp_server.cc文件(服务端源文件):

#include "tcp_server.hpp"
#include <memory>void Usage(const char *proc)
{std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}int main(int argc, const char **argv)
{if(argc != 2){Usage(argv[0]);exit(1);}std::unique_ptr<TcpServer> ptr_srv(new TcpServer(atoi(argv[1])));ptr_srv->init();ptr_srv->start();return 0;
}

6.2 TCP协议客户端的封装

TCP协议客户端,需要先调用connect函数,尝试与服务器建立链接,才能与服务器正常通信。

运行TCP协议客户端的时候,需要显示给的IP地址和服务器对应的端口号。

tcp_client.hpp文件(客户端封装):

#pragma once#include "Log.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>class TcpClient
{
public:TcpClient(const std::string& ip, uint16_t port): _ip(ip), _port(port), _sock(-1){ }// 初始化客户端void init(){// 创建socket文件描述符_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));exit(2);}logMessage(NORMAL, "socket success, _sock:%d\n", _sock);// 客户端与服务端连接struct sockaddr_in sock_srv;memset(&sock_srv, 0, sizeof(sock_srv));sock_srv.sin_family = AF_INET;sock_srv.sin_addr.s_addr = inet_addr(_ip.c_str());sock_srv.sin_port = htons(_port);socklen_t len = sizeof(sock_srv);if(connect(_sock, (struct sockaddr *)&sock_srv, len) < 0){logMessage(FATAL, "connect error, %d:%s\n", errno, strerror(errno));exit(3);}logMessage(NORMAL, "connect success\n");logMessage(NORMAL, "Client Init Success\n");}void start(){std::string msg;    // 发送的消息char buffer[1024];  // 接受服务器发回的消息while(true){std::cout << "请输入你要发送的消息: " << std::flush;std::getline(std::cin, msg);if(msg == "quit")break;ssize_t n = send(_sock, msg.c_str(), msg.size(), 0);if(n > 0){logMessage(NORMAL, "成功发送数据!\n");ssize_t s = recv(_sock, buffer, 1023, 0);if(s > 0){buffer[s] = '\0';printf("回显# %s\n", buffer);}else if(s == 0){logMessage(DEBUG, "服务器退出!\n");break;}else{logMessage(DEBUG, "获取服务器发回数据失败!\n");continue;}}}}~TcpClient(){if(_sock < 0){close(_sock);}}private:std::string _ip;uint16_t _port;int _sock;
};

tcp_client.cc文件(客户端源文件):

#include "tcp_server.hpp"
#include <memory>void Usage(const char *proc)
{std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}int main(int argc, const char **argv)
{if(argc != 2){Usage(argv[0]);exit(1);}std::unique_ptr<TcpServer> ptr_srv(new TcpServer(atoi(argv[1])));ptr_srv->init();ptr_srv->start();return 0;
}

七. 总结

  • 在网络中,每一台主机都有一个独立的ip地址,每台主机上的进程都可以对应与一个或多个端口号,但是一个端口号不能够对应多个进程。
  • 网络通信,本质上是网络中两台主机上的相应进程之间的进程间通信,通过 IP + Port,可以标识全网中唯一的一个进程。
  • 主机字节序有大端和小端之分,网络数据统一采用大端字节序,进行网络通信时,必须调用相应的接口函数,来实现网络格式和主机格式之间的转换。
  • sockaddr结构体可用于网络(本地)通信,表示套接字,struct sockaddr_in专门用于网络通信,其成员包括网络协议族(AF_INET、AF_INET6、AF_LOCAL等)、主机ip以及端口号,用于socket编程的函数,接受的参数均为struct sockaddr类型。
  • UDP协议全称用户数据报协议,使用UDP协议通信,不需要通信双方建立连接,通信不可靠,可能出现丢包、失帧等问题,但是UDP协议通信的成本较低,因此依旧存在广泛的应用。
  • TCP协议全称传输控制协议,通信双方需要再建立好连接之后才可以通信,通信过程可靠,不会出现丢包等问题,但是可靠通信的成本也相对较高。

相关文章:

Linux网络编程:使用UDP和TCP协议实现网络通信

目录 一. 端口号的概念 二. 对于UDP和TCP协议的认识 三. 网络字节序 3.1 字节序的概念 3.2 网络通信中的字节序 3.3 本地地址格式和网络地址格式 四. socket编程的常用函数 4.1 sockaddr结构体 4.2 socket编程常见函数的功能和使用方法 五. UDP协议实现网络通信 5.…...

【后端速成 Vue】初识指令(上)

前言&#xff1a; Vue 会根据不同的指令&#xff0c;针对标签实现不同的功能。 在 Vue 中&#xff0c;指定就是带有 v- 前缀 的特殊 标签属性&#xff0c;比如&#xff1a; <div v-htmlstr> </div> 这里问题就来了&#xff0c;既然 Vue 会更具不同的指令&#…...

爬虫 — Scrapy-Redis

目录 一、背景1、数据库的发展历史2、NoSQL 和 SQL 数据库的比较 二、Redis1、特性2、作用3、应用场景4、用法5、安装及启动6、Redis 数据库简单使用7、Redis 常用五大数据类型7.1 Redis-String7.2 Redis-List (单值多value)7.3 Redis-Hash7.4 Redis-Set (不重复的)7.5 Redis-Z…...

tcpdump常用命令

需要安装 tcpdump wireshark ifconfig找到网卡名称 eth0, ens192... tcpdump需要root权限 网卡eth0 经过221.231.92.240:80的流量写入到http.cap tcpdump -i eth0 host 221.231.92.240 and port 80 -vvv -w http.cap ssh登录到主机查看排除ssh 22端口的报文 tcpdump -i …...

计算机网络运输层网络层补充

1 CDMA是码分多路复用技术 和CMSA不是一个东西 UPD是只确保发送 但是接收端收到之后(使用检验和校验 除了检验的部分相加 对比检验和是否相等。如果不相同就丢弃。 复用和分用是发生在上层和下层的问题。通过比如时分多路复用 频分多路复用等。TCP IP 应用层的IO多路复用。网…...

java CAS详解(深入源码剖析)

CAS是什么 CAS是compare and swap的缩写&#xff0c;即我们所说的比较交换。该操作的作用就是保证数据一致性、操作原子性。 cas是一种基于锁的操作&#xff0c;而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住&#xff0c;等之前获得锁的线程释放锁之后&am…...

1786_MTALAB代码生成把通用函数生成独立文件

全部学习汇总&#xff1a; GitHub - GreyZhang/g_matlab: MATLAB once used to be my daily tool. After many years when I go back and read my old learning notes I felt maybe I still need it in the future. So, start this repo to keep some of my old learning notes…...

2023/09/19 qt day3

头文件 #ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QDebug> #include <QTime> #include <QTimer> #include <QPushButton> #include <QTextEdit> #include <QLineEdit> #include <QLabel> #include &l…...

Docker 学习总结(78)—— Docker Rootless 让你的容器更安全

前言 在以 root 用户身份运行 Docker 会带来一些潜在的危害和安全风险,这些风险包括: 容器逃逸:如果一个容器以 root 权限运行,并且它包含了漏洞或者被攻击者滥用,那么攻击者可能会成功逃出容器,并在宿主系统上执行恶意操作。这会导致宿主系统的安全性受到威胁。 特权升…...

如何使用ArcGIS Pro将等高线转DEM

通常情况下&#xff0c;我们拿到的等高线数据一般都是CAD格式&#xff0c;如果要制作三维地形模型&#xff0c;使用栅格格式的DEM数据是更好的选择&#xff0c;这里就为大家介绍一下如何使用ArcGIS Pro将等高线转DEM&#xff0c;希望能对你有所帮助。 创建TIN 在工具箱中选择“…...

【爬虫基础】万字长文详解XPath

1. 引言 XPath&#xff08;XML Path Language&#xff09;是一种在XML和HTML文档中查找和定位信息的强大工具。XPath的重要性在于它允许我们以简洁而灵活的方式导航和选择文档中的元素和属性。本文将深入介绍XPath的基础知识&#xff0c;帮助你掌握这个强大的查询语言&#xf…...

分布式多级缓存SDK设计的思考

分布式多级缓存SDK设计的思考 背景整体架构多层级组装回调埋点分区处理一致性问题缓存与数据库之间的一致性问题不同层级缓存之间的一致性问题不同微服务实例上&#xff0c;非共享缓存之间的一致性问题 小结 之前实习期间编写过一个简单的多级缓存SDK&#xff0c;后面了解到一些…...

设计模式:适配器模式(C++实现)

适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构设计模式&#xff0c;它允许将一个类的接口转换成客户端所期望的另一个接口。适配器模式通常用于连接两个不兼容的接口或类&#xff0c;使它们能够一起工作。 以下是一个简单的C适配器模式的示例&#xff1a; #in…...

【深度学习实验】前馈神经网络(二):使用PyTorch实现不同激活函数(logistic、tanh、relu、leaky_relu)

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 定义激活函数 logistic(z) tanh(z) relu(z) leaky_relu(z, gamma0.1) 2. 定义输入、权重、偏置 3. 计算净活性值 4. 绘制激活函数的图像 5. 应用激活函数并…...

容器技术所涉及Linux内核关键技术

一、容器技术前世今生 1.1 1979年 — chroot 容器技术的概念可以追溯到1979年的UNIX chroot。 它是一套“UNIX操作系统”系统&#xff0c;旨在将其root目录及其它子目录变更至文件系统内的新位置&#xff0c;且只接受特定进程的访问。 这项功能的设计目的在于为每个进程提供…...

IPV4和IPV6,公网IP和私有IP有什么区别?

文章目录 1、什么是IP地址&#xff1f;1.1、背景1.2、交换机1.3、局域网1.4、广域网1.5、ISP 互联网服务提供商 2、IPV42.1、什么是IPV4&#xff1f;2.2、IPV4的组成2.3、NAT 网络地址转换2.4、端口映射 3、公网IP和私有IP4、IPV6 1、什么是IP地址&#xff1f; 1.1、背景 一台…...

高云FPGA系列教程(7):ARM GPIO外部中断

文章目录 [toc]GPIO中断简介FPGA配置常用函数MCU程序设计工程下载 本文是高云FPGA系列教程的第7篇文章。 本篇文章介绍片上ARM Cortex-M3硬核处理器GPIO外部的使用&#xff0c;演示按键中断方式来控制LED亮灭&#xff0c;基于TangNano 4K开发板。 参考文档&#xff1a;Gowin_E…...

Python爬虫:动态获取页面

动态网站根据用户的某些操作产生一些结果。例如&#xff0c;当网页仅在向下滚动或将鼠标移动到屏幕上时才完全加载时&#xff0c;这背后一定有一些动态编程。当您将鼠标指针悬停在某些文本上时&#xff0c;它会为您提供一些选项&#xff0c;它还包含一些动态.这是是一篇关于动态…...

大数据平台迁移后yarn连接zookeeper 异常分析

大数据平台迁移后yarn连接zookeeper 异常分析 XX保险HDP大数据平台机房迁移异常分析。 异常现象&#xff1a; 机房迁移后大部分组件都能正常启动Yarn 启动后8088 8042等端口无法访问Hive spark 作业提交到yarn会出现卡死。 【备注】虽然迁移&#xff0c;但IP不变。 1. Yarn连…...

Ubuntu Nginx 配置 SSL 证书

首先需要在 Ubuntu 中安装 Nginx 服务, 打开终端执行以下命令: $ sudo apt update $ sudo apt install nginx -y然后启动 Nginx 服务并设置为开机时自动启动, 执行以下命令: $ sudo systemctl start nginx $ sudo systemctl enable nginx最后再验证一下 Nginx 服务的当前状态…...

将本地前端工程中的npm依赖上传到Nexus

【问题背景】 用Nexus搭建了内网的依赖仓库&#xff0c;需要将前端工程中node_modules中的依赖上传到Nexus上&#xff0c;但是node_modules中的依赖已经是解压后的状态&#xff0c;如果直接机械地将其简单地打包上传到Nexus&#xff0c;那么无法通过npm install下载使用。故有…...

软考高级架构师下篇-16通信系统架构设计理论与实践

目录 1. 引言2. 通信系统网络架构3. 网络构建关键技术4. 网络构建5. 前文回顾1. 引言 此章节主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本节知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中…...

国庆中秋特辑(二)浪漫祝福方式 使用生成对抗网络(GAN)生成具有节日氛围的画作

要用人工智能技术来庆祝国庆中秋&#xff0c;我们可以使用生成对抗网络&#xff08;GAN&#xff09;生成具有节日氛围的画作。这里将使用深度学习框架 TensorFlow 和 Keras 来实现。 一、生成对抗网络&#xff08;GAN&#xff09; 生成对抗网络&#xff08;GANs&#xff0c;…...

stm32 串口发送和接收

串口发送 #include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h>//初始化串口 void Serial_Init() {//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Pe…...

Vite + Vue3 实现前端项目工程化

通过官方脚手架初始化项目 第一种方式&#xff0c;这是使用vite命令创建&#xff0c;这种方式除了可以创建vue项目&#xff0c;还可以创建其他类型的项目&#xff0c;比如react项目 npm init vitelatest 第二种方式&#xff0c;这种方式是vite专门为vue做的配置&#xff0c;…...

Java动态代理Aop的好处

1. 预备知识-动态代理 1.1 什么是动态代理 动态代理利用Java的反射技术(Java Reflection)生成字节码&#xff0c;在运行时创建一个实现某些给定接口的新类&#xff08;也称"动态代理类"&#xff09;及其实例。 1.2 动态代理的优势 动态代理的优势是实现无侵入式的代…...

各种存储性能瓶颈如何分析与优化?

【摘要】本文结合实践剖析存储系统的架构及运行原理&#xff0c;深入分析各种存储性能瓶颈场景&#xff0c;并提出相应的性能优化手段&#xff0c;希望对同行有一定的借鉴和参考价值。 【作者】陈萍春&#xff0c;现就职于保险行业&#xff0c;拥有多年的系统、存储以及数据备…...

Android StateFlow初探

Android StateFlow初探 前言&#xff1a; 最近在学习StateFlow&#xff0c;感觉很好用&#xff0c;也很神奇&#xff0c;于是记录了一下. 1.简介&#xff1a; StateFlow 是一个状态容器式可观察数据流&#xff0c;可以向其收集器发出当前状态更新和新状态更新。还可通过其 …...

Docker Compose初使用

简介 Docker-Compose项目是Docker官方的开源项目&#xff0c;负责实现对Docker容器集群的快速编排。 Docker-Compose将所管理的容器分为三层&#xff0c;分别是 工程&#xff08;project&#xff09;&#xff0c;服务&#xff08;service&#xff09;以及容器&#xff08;cont…...

测试与FastAPI应用数据之间的差异

【squids.cn】 全网zui低价RDS&#xff0c;免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 当使用两个不同的异步会话来测试FastAPI应用程序与数据库的连接时&#xff0c;可能会出现以下错误&#xff1a; 在测试中&#xff0c;在数据库中创建了一个对象&#x…...

宁波公司做网站/企点qq官网

备注&#xff1a;NFS 文件共享需设置两部分&#xff1a;服务端和客户端 一、服务端设置 1.1、查看nfs包是否安装&#xff0c;未安装则重新安装 [rootlocalhost ~]# rpm -qa|grep rpcbindrpcbind-0.2.0-42.el7.x86_64[rootlocalhost ~]# rpm -qa|grep nfs-utilsnfs-utils-1.3.0-…...

河北外贸网站建设/搜索引擎调价工具哪个好

ssl证书是一种安全证书&#xff0c;这也是数字证书的一种&#xff0c;一般都是使用于网站的服务器当中的&#xff0c;因此又被称之服务器证书。很多企业都会给网站配置ssl证书&#xff0c;目的是为了保护网站信息安全&#xff0c;防止信息被不法分子盗用。因此&#xff0c;ssl加…...

深圳高端品牌网站设计/北京做网站推广

DataGrid使用心得 原文地址&#xff1a;http://dotnet.chinaitlab.com/ADONET/398553.html1.为DataGrid控件设计样式 在<asp:datagridid"DataGrid1"runat"server">之后添加如下代码<FooterStyleForeColor"Black"BackColor"#CCCC…...

网站开发能作为无形资产吗/广告公司网站制作

...

义乌网站建设方式/b站黄页推广软件

RelativeLayout布局 android:layout_marginTop"25dip" //顶部距离 android:gravity"left" //空间布局位置 android:layout_marginLeft"15dip //距离左边距 // 相对于给定ID控件 android:layout_above 将该控件的底部置于给定ID的控件之上; android:la…...

商业营销厅装修公司/seo快速排名代理

使用 gensim 训练中文词向量&#xff0c;计算词语之间的相似度。 输入&#xff1a;语料库&#xff0c;txt文件。 输出&#xff1a;余弦相似度。 实现代码&#xff1a; # -*- coding: utf-8 -*-import loggingfrom gensim import models from gensim.models import word2vec…...