手工艺品出口网站建设策划书,自己的电脑做服务器建立网站的方法,qq推广中心,磁力搜索神器文章目录 1. 前言1.1 网络方面的预备知识#x1f447;1.2 了解TCP协议 2. 关于套接字编程2.1 什么是套接字 Socket2.2 socket 的接口函数2.3 Udp套接字编程的步骤2.4 sockaddr 结构 3. 代码实现3.1 makefile3.2 log.hpp3.3 tcp_server.hpp① 框架② service() 通信服务③ init… 文章目录 1. 前言1.1 网络方面的预备知识1.2 了解TCP协议 2. 关于套接字编程2.1 什么是套接字 Socket2.2 socket 的接口函数2.3 Udp套接字编程的步骤2.4 sockaddr 结构 3. 代码实现3.1 makefile3.2 log.hpp3.3 tcp_server.hpp① 框架② service() 通信服务③ initServer()初始化服务器④ startServer()启动服务器 3.4 tcp_server.cc3.5 tcp_client.cc3.6 结果演示 4. 线程池版本实现英汉互译4.1 线程池ThreadPool的实现Task.hpp 4.2 对多进程代码的修改 - TcpServertcp_server.hpp框架service() 通信服务dictOnline()英汉互译/网络词典initServer()初始化服务器startServer()启动服务器 结果演示 5. 完整代码 1. 前言
1.1 网络方面的预备知识 网络基础 - 预备知识协议、网络协议、网络传输流程、地址管理 1.2 了解TCP协议
首先我们对Tcp协议进行一个了解 【网络基础】深入理解TCP协议协议段、可靠性、各种机制 简单总结而言 TCP协议具有以下特点
1.可靠传输TCP通过三次握手建立连接并使用序号、确认ACK和重传机制来确保数据的可靠性传输。 2.面向连接在数据传输之前TCP必须先建立连接双方通过握手过程建立起通信信道。 3.数据流式传输TCP将数据拆分成小块并通过IP地址和端口号标识发送和接收方的数据包保证数据的有序传输。 4.拥塞控制和流量控制TCP具有拥塞控制和流量控制机制通过调整发送方的发送速率和接收方的反馈机制来避免网络拥塞和资源浪费。 2. 关于套接字编程
2.1 什么是套接字 Socket 套接字Socket 是计算机网络中用于实现进程间通信的一种机制。它允许在不同计算机之间或同一计算机的不同进程之间进行数据传输和通信。 套接字可以看作是网络通信中的一个端点 它由 IP地址 和 端口号 组成 用于唯一标识网络中的通信实体点 。套接字提供了一组接口通常是API用于创建、连接、发送、接收和关闭连接等操作以实现数据的传输和通信。 套接字可以分为两种类型了解 流套接字Stream Socket 和 数据报套接字Datagram Socket 。 流套接字 基于 传输控制协议TCP 的套接字提供面向连接的、可靠的、双向的数据传输。 流套接字通过建立连接来实现数据的可靠传输适用于需要保证数据完整性和顺序性的应用如网页浏览、文件传输等。 数据报套接字基于 用户数据报协议UDP 的套接字提供无连接的、不可靠的数据传输。 数据报套接字不需要建立连接可以直接发送数据报给目标主机适用于实时性要求高、对数据完整性和顺序性要求不高的应用如视频流传输、实时游戏等。 2.2 socket 的接口函数
下面列举在我们进行Tcp与Udp的套接字编程所用的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.3 Udp套接字编程的步骤 创建套接字使用 socket() 函数创建套接字。 绑定套接字利用 bind() 将套接字绑定到一个 IP 地址和端口上。 监听连接对于服务器端使用 listen() 开始监听连接请求。 接受连接对于服务器端使用 accept() 接受客户端的连接请求并创建新的套接字用于通信。 发送数据使用 send() 函数发送数据到连接的另一端。 接收数据使用 recv() 函数接收从连接的另一端发送过来的数据。 关闭连接通信结束后使用 close() 关闭连接的套接字以释放资源。 2.4 sockaddr 结构
首先
IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6 这样只要取得某种sockaddr结构体的首地址不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。 socket API 可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 好处在于程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数; sockaddr 结构是在套接字编程中表示网络地址的通用结构。本身是一个抽象的结构最常见的使用是用于表示 IPv4 地址。
sockaddr 结构定义如下
struct sockaddr {sa_family_t sa_family; // 地址家族如 AF_INETchar sa_data[14]; // 地址数据
};在实际使用中经常使用 struct sockaddr_in 结构来表示 IPv4 地址定义如下
struct sockaddr_in {sa_family_t sin_family; // 地址家族AF_INETin_port_t sin_port; // 端口号struct in_addr sin_addr; // IP 地址char sin_zero[8]; // 填充字段通常为0
};3. 代码实现
3.1 makefile
首先写一个简单的makefile文件用于后续执行程序便于测试
.PHONY:all
all:tcp_client tcp_servertcp_client:tcp_client.ccg -o $ $^ -stdc11 #-lpthread
tcp_server:tcp_server.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -f tcp_client tcp_server3.2 log.hpp
在我们编写代码时对于 异常情况报告或正常情况通知 利用log.hpp进行日志信息的记录
#pragma once#include iostream
#include cstdio
#include cstdarg#include log.hpp// 宏定义 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4// 全局字符串数组 : 将日志级别映射为对应的字符串
const char *gLevelMap[] {DEBUG,NORMAL,WARNING,ERROR,FATAL
};#define LOGFILE ./threadpool.log // LOGFILE: 表示日志文件的路径void logMessage(int level, const char* format, ...)
{// 判断DEBUG_SHOW 是否定义分别执行操作#ifndef DEBUG_SHOW // 将日志级别映射为对应的字符串if(level DEBUG) return; // DEBUG_SHOW不存在 且 日志级别为 DEBUG时返回
#endif// DEBUG_SHOW存在 则执行下面的日志信息 char stdBuffer[1024];time_t timestamp time(nullptr);// 将日志级别和时间戳格式化后的字符串将会被写入到 stdBuffer 缓冲区中snprintf(stdBuffer, sizeof(stdBuffer), [%s] [%ld] , gLevelMap[level], timestamp);char logBuffer[1024];va_list args;在这里插入代码片va_start(args, format);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args);printf(%s%s\n, stdBuffer, logBuffer);
}对于该日志类不再重点根据需要可以自行调整编写不多讲解。 3.3 tcp_server.hpp
① 框架
tcp_server.hpp 即服务器类首先是对于该服务器的 框架
该框架包含了我们实现该类中会编写的 相关函数 以及 成员变量 。
// 通信服务的代码
static void service(int sock, const string clientip, const int16_t clientport){}class TcpServer // tcp服务器类
{
private:const static int gbacklog 20;public:TcpServer(uint16_t port, string ip ): _port(port), _ip(ip), listensock(-1) // 设定缺省值{}// 初始化服务器void initServer(){}// 启动服务器void startServer(){}~TcpServer(){}private:uint16_t _port; // 端口号string _ip; // ip地址int listensock; // 套接字
};② service() 通信服务
该代码用于 处理客户端与服务器间的通信
读取sock的内容 读取成功则打印出客户端信息与发送的内容读取失败打印日志并退出 最后回显内容给客户端并关闭sock
static void service(int sock, const string clientip, const int16_t clientport)
{char buffer[1024];while(true){// 网络通信 可以直接使用read/writessize_t s read(sock, buffer, sizeof(buffer) - 1);if(s 0) {// 成功读取buffer[s] 0;cout clientip : clientport # buffer endl;}else if (s 0){ // 对端关闭了连接logMessage(NORMAL, %s:%d shutdown, metoo, clientip.c_str(), clientport);break;}else{ // 错误logMessage(ERROR, read socket error, %d:%s, errno, strerror(errno));break;}// 读取成功 将buffer内容写入sock回显write(sock, buffer, strlen(buffer));}close(sock);
}③ initServer()初始化服务器
下面是 初始化服务器 的代码简单描述其步骤
socket()创建监听套接字bind()绑定客户端端口与ip用listen()设置监听状态因为tcp是面向连接的需要先建立连接才能进行通信
void initServer(){// 1. 创建socket —— 进程、文件方面listensock socket(AF_INET, SOCK_STREAM, 0); // ipv4协议套接字类型协议类型if(listensock 0) // 创建套接字失败{logMessage(FATAL, %d : %s, errno, strerror(errno));exit(2); // 退出进程}logMessage(NORMAL, create socket success, sock: %d, listensock); // 创建成功输出信息// 2. bind —— 网络、文件方面struct sockaddr_in local; // 表示ipv4地址memset(local, 0, sizeof(local)); // 初始化local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());// 3. 设置监听状态// TCP面向连接正式通信前应先建立连接if(listen(listensock, gbacklog) 0){logMessage(FATAL, listen error: %d:%s, errno, strerror(errno));exit(4);}logMessage(NORMAL, init server success.);} ④ startServer()启动服务器
下面是启动服务器的代码简述其步骤 首先下面代码分为 单进程与多进程 版本在注释有标出
通过accept()来 获取连接 获取连接成功后 进行双方的通信 对于单进程提取客户端的ip与port后直接调用之前的service()即可对于多进程首先通过fork创建子进程进行功能分配。 从功能上讲 单进程版本 单个进程负责监听套接字并处理连接请求在处理连接的过程中单个进程可能会阻塞在读取或写入数据的操作上导致无法及时处理其他连接请求。 多进程版本 在父进程中负责监听套接字并循环接收连接请求。每当有新的连接请求到来时父进程会创建一个新的子进程来处理该连接。子进程独立于父进程负责与客户端进行通信父进程则继续监听新的连接请求。每个子进程都有自己的资源空间因此可以独立处理连接避免了单进程版本中可能出现的阻塞问题提高了并发处理能力。
void startServer()
{//此signal : 多线程代码signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号while(true){// 4. 获取连接struct sockaddr_in src;socklen_t len sizeof(src);int servicesock accept(listensock, (struct sockaddr*)src, len);if(servicesock 0){logMessage(ERROR, accept error, %d:%s, errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port ntohs(src.sin_port);string client_ip inet_ntoa(src.sin_addr);logMessage(NORMAL, link success, servicesock: %d| %s : %d |\n, servicesock, client_ip.c_str(), client_port);// 开始通信// v1: 单进程循环// service(servicesock, client_ip, client_port);// v2: 多进程// 子进程用于给新的连接提供服务(service)父进程继续执行循环接收连接pid_t id fork();assert(id ! -1);if(id 0){// 子进程: 用于提供服务无需监听socketclose(listensock);service(servicesock, client_ip, client_port);exit(0); // 此时僵尸状态}close(servicesock); // 父进程不提供服务}
}3.4 tcp_server.cc
tcp_server.cc 用于 形成最后的可执行程序 在main函数中 初始化服务器并启动服务器即可对于TcpServer对象的创建我们可以使用unique_ptr智能指针进行创建对象用于在动态内存中分配对象可以在不需要时自动释放内存。通过获取到的参数创建对象
using std::cout;
using std::endl;static void usage(string proc)
{cout \nUsage: proc port endl;
}int main(int argc, char* argv[])
{if(argc ! 2) { // 参数数量错误usage(argv[0]); // 输出正确使用方法exit(1); // 退出}uint16_t port atoi(argv[1]); // 获取端口号// 智能指针创建 TcpServer对象std::unique_ptrTcpServer svr(new TcpServer(port));svr-initServer();svr-startServer();return 0;
}3.5 tcp_client.cc
同理对于tcp_client.cc首先获取传来的ip与端口号循环内 如果还未建立连接 创建套接字sock 与 sockaddr_in 结构体后进行connect() 连接 建立连接后 持续读取用户输入的内容并接收来自客户端的回显信息。
// 打印正确的程序使用方法
void Usage(std::string proc)
{std::cout \nUsage: proc ip port std::endl;
}// ./tcp_client 192.168.1.100 8080
int main(int argc, char* argv[])
{if(argc ! 3){Usage(argv[0]);exit(1);}std::string serverIp argv[1];uint16_t serverPort atoi(argv[2]);bool connectAlive false; // 是否已经建立了连接int sock 0;while(true){if(!connectAlive){sock socket(AF_INET, SOCK_STREAM, 0);if(sock 0) { // 创建套接字失败std::cerr socket error std::endl;exit(2);}struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_port htons(serverPort);server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(serverIp.c_str());if(connect(sock, (struct sockaddr*)server, sizeof(server)) 0){ // 连接失败std::cerr connect error: errno strerror(errno) std::endl;exit(3);}// 建立连接成功讲connectALive 设为truestd::cout connect success. std::endl;connectAlive true;}std::cout 请输入# std::endl;std::string line;std::getline(std::cin, line);if(line quit) break;ssize_t s send(sock, line.c_str(), line.size(), 0);if(s 0) { // send 成功char buffer[1024];ssize_t r recv(sock, buffer, sizeof(buffer), 0);if(r 0) { // recv 成功buffer[r] \0;std::cout server response: buffer std::endl;} else { // recv 失败std::cerr recv() error: client receiving message failed. std::endl;close(sock);connectAlive false;}} else {std::cerr send() error: client sending message failed. std::endl;close(sock);}}return 0;
}3.6 结果演示
如下图所示当启动客户端后客户端会有一个监听套接字
此时我们可以有多个客户端同时进行连接通信并且能正确的接收到来自服务器的回显信息。 4. 线程池版本实现英汉互译
4.1 线程池ThreadPool的实现
对于线程池版本首先我们需要自实现一个线程池有关线程池的详细内容/ 代码在下面 ThreadPool代码实例 与 理解 而线程池版本的套接字实现我们需要 对上面链接代码部分进行修改 需要根据要求在这里即英汉互译对Task.hpp任务类进行更改
Task.hpp
对于任务类由于我们通信时需要套接字对方的ip、端口号以及处理方法所以 对Task类的变量和成员函数进行修改 即可
#pragma once#include iostream
#include string
#include functional
#include log.hpp// typedef std::functionint(int, int) func_t;
using func_t std::functionvoid(int, const std::string ip, const uint16_t port, const std::string name);class Task
{
public:// 构造Task(){}Task(int sock, const std::string ip, const uint16_t port, func_t func):_sock(sock),_ip(ip),_port(port),_func(func){}void operator()(const std::string name){_func(_sock, _ip, _port, name);}private:int _sock;uint16_t _port;std::string _ip;func_t _func;
};4.2 对多进程代码的修改 - TcpServer
由于我们将改为线程池的版本所以这里我们对服务器的逻辑tcp_server.hpp作修改
tcp_server.hpp
框架
框架用于展示整个文件中包含的函数以及类的实现省略具体实现
#pragma once#include iostream
#include string
#include string.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.h
#include assert.h
#include signal.h
#include memory
#include unordered_map#include ThreadPool/threadPool.hpp
#include ThreadPool/log.hpp
#include ThreadPool/Task.hpp// 通信服务
static void service(int sock, const std::string clientip, const uint16_t clientport, const std::string thread_name)
{}// 英汉互译
static void dictOnline(int sock, const std::string clientip, const uint16_t clientport, const std::string thread_name)
{}class TcpServer
{
private:const static int gbacklog 20;public: // 构造 析构TcpServer(const uint16_t port, const std::string ip 0.0.0.0): _ip(ip), _port(port),_listensock(-1), _threadpool_ptr(ThreadPoolTask::getThreadPool()){}~TcpServer(){}// 功能函数// 启动服务器void initServer(){ }void startServer(){ }private:std::string _ip;uint16_t _port;int _listensock;std::unique_ptrThreadPoolTask _threadpool_ptr;
};service() 通信服务
对于通信服务部分总体和多进程版本无异区别在于参数上 传入了线程名 可以在输出消息时加上线程名。
static void service(int sock, const std::string clientip, const uint16_t clientport, const std::string thread_name)
{char buffer[1024];while(true){ssize_t s read(sock, buffer, sizeof(buffer) - 1);if(s 0){buffer[s] 0;std::cout thread_name | clientip : clientport | buffer std::endl; }else if (s 0){logMessage(DEBUG, client quit, me too.);break;}else{logMessage(ERROR, read error, me too.);break;}write(sock, buffer, strlen(buffer)); // 回写内容}close(sock);
}dictOnline()英汉互译/网络词典
对于该部分主要包含以下步骤
创建一个哈希类作为词典用于对应中英文随后循环读取客户端的内容 如果可以在词典中找到就记录该对应的词汇如果找不到就打印错误信息 最后将结果写回客户端
static void dictOnline(int sock, const std::string clientip, const uint16_t clientport, const std::string thread_name)
{char buffer[1024];static std::unordered_mapstd::string, std::string dict {{hello, 你好},{world, 世界},{tcp, 传输控制协议},{udp, 用户数据报协议}};// 添加4组数据dict[rpc] 远程过程调用;dict[ssh] 安全外壳;dict[http] 超文本传输协议;dict[https] 超文本传输协议安全;while(true){ssize_t s read(sock, buffer, sizeof(buffer) - 1);if(s 0){buffer[s] 0;std::string message;auto iter dict.find(buffer);if(iter dict.end()) message not found:(;else message iter-second;std::cout thread_name | clientip : clientport | buffer | message std::endl;write(sock, message.c_str(), message.size()); // 将结果写回}else if (s 0){logMessage(DEBUG, client quit, me too.);break;}else{logMessage(ERROR, read error | %d : %s, errno, strerror(errno));break;} }close(sock);
}initServer()初始化服务器
初始化服务器的代码与多进程版本一致
void initServer(){// 创建listensock_listensock socket(AF_INET, SOCK_STREAM, 0);if(_listensock 0) {logMessage(ERROR, socket error, %d : %s, errno, strerror(errno));exit(1);}// 绑定struct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr inet_addr(_ip.c_str());if(bind(_listensock, (struct sockaddr*)local, sizeof(local)) 0){logMessage(ERROR, bind error, %d : %s, errno, strerror(errno));exit(2);}// 监听if(listen(_listensock, gbacklog) 0){logMessage(ERROR, listen error, %d : %s, errno, strerror(errno));exit(3);}logMessage(NORMAL, init server success.);}startServer()启动服务器
对于启动服务器首先需要 运行线程池 在正确建立连接后将客户端的ip端口号与英汉互译的功能一同 传入Task对象最后将任务类添加到线程池中
void startServer()
{// 使用线程池_threadpool_ptr-run();while(true){// 等待客户端连接struct sockaddr_in peer;socklen_t len sizeof(peer);int sock accept(_listensock, (struct sockaddr*)peer, len);if(sock 0){logMessage(ERROR, accept error, %d : %s, errno, strerror(errno));continue;}// 成功获取连接uint16_t client_port ntohs(peer.sin_port);std::string client_ip inet_ntoa(peer.sin_addr);logMessage(NORMAL, get a new client, ip: %s, port: %d, client_ip.c_str(), client_port);// 创建线程Task t(sock, client_ip, client_port, dictOnline);_threadpool_ptr-pushTask(t); // 添加 任务}
}结果演示
通过下图可以看出启动服务器后服务器可以接收多个客户端的信息并正确发出反馈。 5. 完整代码
上述关于Tcp套接字通信的完整代码在 Tcp套接字编程 - 实例代码