网络编程套接字
文章目录
- 1. socket编程接口
- 1-1 socket 常见API
- 1-2 sockaddr结构
- 2. 简单的UDP网络程序
- 2-1 日志(固定用法:标准部分+自定义部分)
- 2-2 服务器代码实现
- 1. 框架
- 2. 初始化服务器
- 3. 服务器运行
- 4. 调用服务器封装函数(UdpServer)
- 2-3 客户端代码
- 2-4 结果展示
- 2-5 改进服务器
- 2-6 改进用户
- 1. 封装线程
- 2. 创建两个线程(一个接受信息一个发送信息)
- 2-7 改进后群聊功能展示
- 2-8 总代码链接
- 3. 简单的TCP网络程序
- 3-1 日志(固定用法参考上面2.1)
- 3-2 服务器代码实现
- 1. 框架
- 2. 初始化服务器
- 3. 调用服务器封装函数(TcpServer)
- 4. 服务器运行(铺垫)
- 5. 服务器运行(多进程版)
- 6. 服务器运行(多线程版)
- 3-3 客户端代码
- 3-4 结果展示
- 3-5 总代码链接
1. socket编程接口
1-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);
1-2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain
Socket.
各种网络协议的地址格式并不相同;为了让接口统一我们根据前16位作区别(就好像原始模板划分为两个不同类型模块)
- 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 结构
/* Structure describing a generic socket address. */
struct sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};
sockaddr_in 结构
/* Structure describing an Internet socket address. */
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)];};
- 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
in_addr结构
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;};
- in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
2. 简单的UDP网络程序
2-1 日志(固定用法:标准部分+自定义部分)
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>using std::cout;
using std::endl;
using std::string;// 日志是有日志级别的
#define DEBUG 0 // 调试
#define NORMAL 1 // 正常
#define WARNING 2 // 警告
#define ERROR 3 // 错误
#define FATAL 4 // 致命错误static const size_t BUFFER_NUM = 1024;
const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"};// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void LogMessage(int level, const char *format, ...) // 可变参数模板
{char stdBuffer[BUFFER_NUM]; // 标准部分const time_t timestamp = time(nullptr);struct tm *L_Time = localtime(×tamp);string time;time += "日期-时间:" + std::to_string(L_Time->tm_year+1900) + "/" + std::to_string(L_Time->tm_mon) + "/" + std::to_string(L_Time->tm_mday) + "-" + std::to_string(L_Time->tm_hour) + ":" + std::to_string(L_Time->tm_min) + ":" + std::to_string(L_Time->tm_sec);std::to_string(L_Time->tm_sec);snprintf(stdBuffer, sizeof stdBuffer, "[%s][%s]",gLevelMap[level], time.c_str());char logBuffer[BUFFER_NUM]; // 自定义部分va_list args;va_start(args, format);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args);printf("%s%s\n", stdBuffer, logBuffer);
}
2-2 服务器代码实现
1. 框架
#ifndef _UDP_SERVERHPP
#define _UDP_SERVERHPP#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include "Log.hpp"using std::cout;
using std::endl;
using std::string;static const size_t SIZE = 1024;
class UdpServer
{
public:UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip){}// 初始化bool InitServer(){}// 服务器运行void start(){}~UdpServer(){if(_sock>0) close(_sock); // 通过文件描述符关闭文件}private:// 一个服务器,一般必须需要ip地址和port(16位的整数)uint16_t _port;string _ip;int _sock = -1; // 创建 socket 文件描述符 的返回值
};#endif
2. 初始化服务器
-
- 创建套接字
- 创建套接字
固定用法AF_INET 和 SOCK_DGRAM;最后一个参数(设置成0)
-
- bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);(1)参数 sockfd ,需要绑定的socket。(文件描述符)(2)参数 addr ,存放了服务端用于通信的地址和端口。(3)参数 addrlen ,表示 addr 结构体的大小(4)返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。
注意:
我们普通用户见得最多的是这种"192.168.110.132"IP地址是点分十进制字符串风格的IP地址每一个区域取值范围是[0-255]: 1字节 -> 4个区域
理论上,表示一个IP地址,其实4字节就够了(减少浪费)
所有在网络上我们需要把点分十进制字符串风格的IP地址 转为4字节再转为网络序列
不过有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据(inet_addr)
用这个INADDR_ANY宏;表示bind任意IP(便于服务器接收数据)
// 初始化bool InitServer(){// 从这里开始,就是新的系统调用,来完成网络功能啦// 1. 完成套接字创建_sock = socket(AF_INET, SOCK_DGRAM, 0);if (_sock < 0) // 创建失败{LogMessage(FATAL, "%d:%s | %s | %d", errno, strerror(errno), __FILE__, __LINE__);exit(2);}// 2. 绑定(将用户设置的ip和port在内核中和我们当前的进程强关联)struct sockaddr_in local;bzero(&local, sizeof local); // 结构体清理(初始化)local.sin_family = AF_INET; // 协议家族local.sin_port = htons(_port); // 网络字节序大端存储// 将点分十进制字符串风格的IP地址 -> 4字节-> 网络序列local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0){// 绑定失败LogMessage(FATAL, "%d:%s | %s | %d", errno, strerror(errno), __FILE__, __LINE__);exit(3);}LogMessage(NORMAL, "init udp server done ... | %s | %d", __FILE__, __LINE__);return true;}
3. 服务器运行
recvfrom () 用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socklen_t *fromlen); (1)ssize_t 相当于 long int,socklen_t 相当于int (2)sockfd:标识一个已连接套接口的描述字。(3)buf:接收数据缓冲区。(4)len:缓冲区长度。(5)flags:调用操作方式。是以下一个或者多个标志的组合体,可通过“ | ”操作符连在一起(通常设置为0)(6)from 是一个指向sockaddr结构体类型的指针;(7)*fromlen表示my_addr结构的长度,可以用sizeof操作符获得。(8)返回值:成功则返回接收到的字符数,失败返回-1.
sendto() 用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作。参数msg指向欲连线的数据内容,参数flags 一般设0
int sendto ( socket s , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen ) ;(1)s:一个用于标识已连接套接口的描述字。(2)buf:包含待发送数据的缓冲区。(3)len:缓冲区中数据的长度。(4)flags:调用执行方式。(5)to 是一个指向sockaddr结构体类型的指针;(5)参数tolen表示to结构的长度,可以用sizeof操作符获得。(6)成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
- 代码块:
// 服务器运行void start(){// 作为一款网络服务器,永远不退出的!// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!char buffer[SIZE]; // 存储读取到的数据for (;;){struct sockaddr_in peer;memset(&peer, 0, sizeof peer);socklen_t len = sizeof peer;// 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0) // 读取成功{// 谁发的数据buffer[s] = 0; // 我们目前数据当做字符串uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的// 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示string cliIP = inet_ntoa(peer.sin_addr);printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);}// end. 写回数据sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);}}
4. 调用服务器封装函数(UdpServer)
#include "udp_server.hpp"
#include <memory>static void Usage(string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}
// ./udp_server 127.0.0.1 8080
int main(int argc, char *argv[]) // 命令行参数
{if (argc != 3){Usage(argv[0]); // 使用手册exit(1);}std::string ip = argv[1];u_int16_t port=atoi(argv[2]);std::unique_ptr<UdpServer> server(new UdpServer(port, ip)); // 智能指针server->InitServer();server->start();return 0;
}
2-3 客户端代码
- 注意:
(1)client要bind,但是一般client不会显示的bind,程序员不会自己bind
(2)client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port; 就芭比Q了
(3)client一般不需要显示的bind指定port,而是让OS自动随机选择(具体在当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT就是调用sendto()函数自动bind)
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>using std::cout;
using std::endl;
using std::string;static const size_t SIZE = 1024;
static void Usage(string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "client: socket error" << endl;exit(2);}// client要要,但是一般client不会显示的bind,程序员不会自己bindstring message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr(argv[1]);char buffer[SIZE];while (true){std::cout << "请输入你的信息# ";std::getline(std::cin, message);if (message == "quit")break;// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORTsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);struct sockaddr_in temp;socklen_t len = sizeof temp;memset(&temp, 0, len);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;cout << "server echo# " << buffer << endl;}std::cerr << "client: srecvfrom error" << endl;}close(sock);return 0;
}
2-4 结果展示
2-5 改进服务器
- 我们不直接绑定服务器;达到如下效果
- 我们需要哈希桶完成映射;并存储用户信息
// 服务器运行void start(){// 作为一款网络服务器,永远不退出的!// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!char buffer[SIZE]; // 存储读取到的数据for (;;){char key[64];struct sockaddr_in peer;memset(&peer, 0, sizeof peer);socklen_t len = sizeof peer;// 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0) // 读取成功{// 谁发的数据buffer[s] = 0; // 我们目前数据当做字符串uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的// 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示string cliIP = inet_ntoa(peer.sin_addr);// printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);snprintf(key, sizeof(key), "%s-%u", cliIP.c_str(), cliPort); // 127.0.0.1-8080LogMessage(NORMAL, "key: %s | %s | %d", key, __FILE__, __LINE__);auto it = _users.find(key);if (it == _users.end()){// 储存LogMessage(NORMAL, "add new user : %s | %s | %d", key, __FILE__, __LINE__);_users[key] = peer;}}// end. 写回数据for (auto &iter : _users){string sendMessage = key;sendMessage += "# ";sendMessage += buffer; // 127.0.0.1-1234# 你好LogMessage(NORMAL, "push message to %s | %s | %d", iter.first.c_str(), __FILE__, __LINE__);sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));}}}
2-6 改进用户
多线程跑起来;实现群聊功能
1. 封装线程
#pragma once#include <iostream>
#include <string>
#include <functional>
#include <cstdio>using std::cout;
using std::endl;
using std::string;static const size_t NAME_NUM = 1024;
typedef void*(*fun_t)(void*); // 线程执行的方法class ThreadDate
{
public:void *_args;string _name;
};class Thread
{
public:Thread(int num, fun_t callback, void *args) : _func(callback){char nameBuffer[NAME_NUM];snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);_tdata._name = nameBuffer;_tdata._args = args;}void start() // 创造线程{pthread_create(&_tid, nullptr, _func, (void *)&_tdata);}void join() // 等待线程{pthread_join(_tid, nullptr);}string &name() // 线程名字{return _tdata._name;}private:fun_t _func;ThreadDate _tdata;pthread_t _tid;
};
2. 创建两个线程(一个接受信息一个发送信息)
- 其实稍微把发送和接受解耦;封装一下就可以了
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "thread.hpp"using std::cout;
using std::endl;
using std::string;uint16_t serverPort = 0;
string serverIp;
static const size_t SIZE = 1024;
static void Usage(string proc)
{cout << "\nUsage: " << proc << " serverIp serverPort\n"<< endl;
}static void *UdpSend(void *args) // 发送消息
{ThreadDate *td = (ThreadDate *)args;int sock = *(int *)td->_args;string name = td->_name;// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bindstring message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverPort);server.sin_addr.s_addr = inet_addr(serverIp.c_str());while (true){std::cerr << "请输入你的信息# ";std::getline(std::cin, message);if (message == "quit")break;// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORTsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);}return nullptr;
}static void *UdpAccept(void *args) // 接受信息
{ThreadDate *td = (ThreadDate *)args;int sock = *(int *)td->_args;string name = td->_name;char buffer[SIZE];while (true){struct sockaddr_in temp;socklen_t len = sizeof temp;memset(&temp, 0, len);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;cout << buffer << endl;}else{std::cerr << "client: srecvfrom error" << endl;}}return nullptr;
}// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "client: socket error" << endl;exit(2);}serverIp = argv[1];serverPort = atoi(argv[2]);std::unique_ptr<Thread> Sender(new Thread(1, UdpSend, (void *)&sock));std::unique_ptr<Thread> Accepter(new Thread(2, UdpAccept, (void *)&sock));// 创建线程Sender->start();Accepter->start();// 等待进程Sender->join();Accepter->join();close(sock); // 关闭文件(sock本质是一个文件)return 0;
}
2-7 改进后群聊功能展示
注意:
- 首次发消息;也再做管理该用户
2-8 总代码链接
链接:
https://gitee.com/ding-xushengyun/linux__cpp/commit/6bfc59b29aae02abecb88d43b1f2b7b4c071fe2c
3. 简单的TCP网络程序
3-1 日志(固定用法参考上面2.1)
3-2 服务器代码实现
1. 框架
- 框架跟UDP几乎相同
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"using std::cout;
using std::endl;
using std::string;static const size_t SIZE = 1024;
class UdpServer
{const static int gbacklog = 20;
public:UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip){}// 初始化bool InitServer(){}// 服务器运行void start(){}~UdpServer(){}private:// 一个服务器,一般必须需要ip地址和port(16位的整数)uint16_t _port;string _ip;int _listenSock = -1; // 监听套接字(建立新链接---客户)
};
2. 初始化服务器
- 初始化:1. 创建套接字(跟上面UDP几乎相同;不同的是第二参数UDP是数据报,我们是面向字节流) 2. 绑定bind;最大不同TCP只不过多了一个聆听- - -listen
- 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接(就好像厨师一直在等待顾客的到来;因为我们不知道顾客什么适合来吃饭)
- 云服务器不能绑定公有IP;需要随机绑定INADDR_ANY
int listen(int sockfd, int backlog);
(1)sockfd 参数表示监听的 socket 句柄
(2)backlog 参数表示接收请求队列的长度(不能太大)
(3)成功则返回 0,失败返回-1,错误原因存于errno 中。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
- 代码块:
void InitServer(){// 1. 创建套接字_listenSock = socket(AF_INET, SOCK_STREAM, 0);if (_listenSock < 0) // 创建失败{LogMessage(FATAL, "创建套接字失败 %d:%s", errno, strerror(errno));exit(2);}LogMessage(FATAL, "创建套接字成功, listensock: %d", _listenSock); // 3// 2. bind 绑定 文件+网络struct sockaddr_in local;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());if (bind(_listenSock, (struct sockaddr *)&local, sizeof local) < 0){LogMessage(FATAL, "绑定失败, %d:%s", errno, strerror(errno));exit(3);}// 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接if (listen(_listenSock, gbacklog) < 0){LogMessage(ERROR, "建立链接失败, %d:%s", errno, strerror(errno));exit(4);}LogMessage(NORMAL, "初始化成功!!!");}
3. 调用服务器封装函数(TcpServer)
- 跟UDP没什么不同
#include <memory>
#include "tcp_server.hpp"static void Usage(string proc)
{std::cout << "\nUsage: " << proc << " port\n"<< std::endl;
}int main(int argc, char* argv[])
{if (argc != 2){Usage(argv[0]); // 使用手册exit(1);}uint16_t port=atoi(argv[1]);std::unique_ptr<TcpServer> sve(new TcpServer(port));sve->InitServer();sve->start();return 0;
}
4. 服务器运行(铺垫)
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
(1)sock 为服务器端套接字
(2)addr 为 sockaddr_in 结构体变量
(3)addrlen 为参数 addr 的长度,可由 sizeof() 求得。
(4)成功则返回 socket(这个sock用来提供服务的),失败返回-1,错误原因存于errno 中。
tcp 面向字节流而且sock还是文件描述符;我们可以把接受和发送数据当作文件处理(read和write)
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{char buffer[SIZE];for (;;){// read && write 可以直接被使用ssize_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, me too!", clientip.c_str(), clientport);break;}else{ //LogMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}
}
- 我们链接(accept)成功后;直接调用service()函数我们得到的只是一个单进程版;什么意思呐?如下图:
(用telnet简单测试一下服务器)我们明显发现只能一个客户发消息;其它客户发消息需要等第一个客户退出才能行。
5. 服务器运行(多进程版)
怎么实现服务器并发处理客户端呐?
- 我们需要让子进程提供服务(调用service函数);父进程监听socket
- 子进程继承父进程的页表;父进程和子进程需要关闭相应文件(子进程:close(_listenSock); 父进程: close(serviceSock);)
- 子进程退出会产生僵尸问题(我们需要父进程不阻塞式处理)
- a. 信号捕捉;主动忽略(signal(SIGCHLD, SIG_IGN);)
- b. 子进程再fork() ,子进程退出;子进程的子进程执行服务(它退出后形成孤儿进程被Init 1一号进程领养)
我们采用信号捕捉的方法
void start(){// 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态signal(SIGCHLD, SIG_IGN);for (;;){// 4. 获取连接struct sockaddr_in src;memset(&src, 0, sizeof 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 clientPort = ntohs(src.sin_port);string clientIp = inet_ntoa(src.sin_addr);LogMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",serviceSock, clientIp.c_str(), clientPort);// 开始进行通信服务啦// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个// service(serviceSock, clientIp, clientPort);// version 2.0 -- 多进程版 --- 创建子进程// 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd 1 0pid_t id = fork();assert(id != -1);if (id == 0){// 子进程:提供服务 不需要监听socketclose(_listenSock);service(serviceSock, clientIp, clientPort);exit(0); // 子进程退出会产生僵尸问题;需要主动忽略信号来解决}close(serviceSock);}}
- telnet简单测试服务器
6. 服务器运行(多线程版)
class ThreadData
{
public:int _sock;std::string _ip;uint16_t _port;
};
- 多线程代码如上所示
- 在多线程这里不用进程关闭特定的文件描述符;因为Linux下的同一个进程中线程共用一张页表
3-3 客户端代码
- TCP和UDP服务器代码都不需要显示绑定bind
- tcp需要链接服务器( connect())
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)sockfd:标识一个套接字。
(2)serv_addr:套接字s想要连接的主机地址和端口号。
(3)addrlen:name缓冲区的长度。
(4)如果链接或者绑定成功则返回 0,失败返回-1,错误原因存于errno 中。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
(1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
(1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
- (长链接)代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"using std::cout;
using std::endl;
using std::string;static const size_t SIZE = 1024;
static void Usage(string proc)
{std::cout << "\nUsage: " << proc << " clientIP port\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]); // 使用手册exit(1);}string ipClient = argv[1];uint16_t portClient = atoi(argv[2]);// 创建字节套接字int sockClient = socket(AF_INET, SOCK_STREAM, 0);if (sockClient < 0) // 创建失败{std::cerr << "client: 创建套接字失败 " << endl;exit(2);}// 不需要显示bind// 但需要建立链接struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(portClient);server.sin_addr.s_addr = inet_addr(ipClient.c_str());if (connect(sockClient, (struct sockaddr *)&server, sizeof server) < 0){std::cerr << "client: 建立链接失败 " << endl;exit(3);}// 建立链接成功string line;while (true){// 通信cout << "请输入# ";std::getline(std::cin, line);if (line == "quit")break;send(sockClient, line.c_str(), line.size(), 0); // write(向sockClient中写数据); 发送数据char buffer[SIZE];ssize_t s = recv(sockClient, buffer, sizeof(buffer) - 1, 0); // read; 接受数据if (s > 0){buffer[s] = 0;cout << "server 回显# " << buffer << endl;}else if (s == 0) // 对写端关闭连接{cout << "shutdown, me too!" << endl;break;}else{std::cerr << "recv socket error" << endl;break;}}return 0;
}
- 短连接代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"using std::cout;
using std::endl;
using std::string;static const size_t SIZE = 1024;
static void Usage(string proc)
{std::cout << "\nUsage: " << proc << " clientIP port\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]); // 使用手册exit(1);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);bool alive = false;int sock = -1;string line;while (true) {if (!alive){sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// client 要不要bind呢?不需要显示的bind,但是一定是需要port// 需要让os自动进行port选择// 连接别人的能力!struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0){std::cerr << "client: 创建套接字失败 " << endl;exit(2);}cout << "链接成功" << endl;alive = true;}// 链接上服务器cout << "请输入# ";std::getline(std::cin, line);if (line == "quit")break;ssize_t s = send(sock, line.c_str(), line.size(), 0);if (s > 0){char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);buffer[s] = 0;cout << "server 回显# " << buffer << std::endl;}else if (s == 0){cout << "shutdown, me too!" << endl;alive = false;close(sock);}else{std::cerr << "recv socket error" << endl;alive = false;close(sock);}}return 0;
}
长链接和短链接在线程池里结果更明显
3-4 结果展示
- 多进程版服务器群聊功能
- 多线程版服务器群聊功能
3-5 总代码链接
链接:
https://gitee.com/ding-xushengyun/linux__cpp/tree/master/tcp
相关文章:

网络编程套接字
文章目录1. socket编程接口1-1 socket 常见API1-2 sockaddr结构2. 简单的UDP网络程序2-1 日志(固定用法:标准部分自定义部分)2-2 服务器代码实现1. 框架2. 初始化服务器3. 服务器运行4. 调用服务器封装函数(UdpServer)…...

海量数据相似数据查询方法
1、海量文本常见 海量文本场景,如何寻找一个doc的topn相似doc,一般存在2个问题, 1)、两两对比时间o(n^2) 2)、高维向量比较比较耗时。 文本集可以看成(doc,word)稀疏矩阵,一般常见的方法是构建到排索引,然后进行归并…...
Codeforces Round #822 (Div. 2)
A(签到) - Select Three Sticks 题意: 给你一个长度为 n 的正整数序列,你可以操作任意次,每一次操作可以选择任意一个元素,把它 1 或者 - 1,问最少多少次操作可以使得序列中存在三个相同的数字以构成一个等边三角形.…...
华为OD机试 - 最短木板长度(JS)
最短木板长度 题目 小明有 n n n块木板,第 i i i(1≤ i i </...

java设计模式——观察者模式
概述 定义:又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。 结构 在观察者模式…...

linux高级命令之线程的注意点
线程的注意点学习目标能够说出线程的注意点1. 线程的注意点介绍线程之间执行是无序的主线程会等待所有的子线程执行结束再结束线程之间共享全局变量线程之间共享全局变量数据出现错误问题2. 线程之间执行是无序的import threading import timedeftask():time.sleep(1)print(&qu…...

MyBatisPlus ---- 多数据源
MyBatisPlus ---- 多数据源1. 创建数据库及表2. 引入依赖3. 配置多数据源4. 创建用户service5. 创建商品service6. 测试适用于多种场景:纯粹多库、读写分离、一主多从、混合模式等 目前我们就来模拟一个纯粹多库的一个场景,其他场景类似 场景说明&#x…...

Java多线程
目录1 多线程1.1 进程1.2 线程1.3 多线程的实现方式1.3.1 方式1:继承Tread类1.3.2 方式2:实现Runnable接口1.3.3 方式3:实现Callable接口1.4 设置和获取线程名称1.5 线程调度1.6 线程控制1.7 线程生命周期1.8 数据安全问题之案例:…...

linux高级命令之线程执行带有参数的任务
线程执行带有参数的任务学习目标能够写出线程执行带有参数的任务1. 线程执行带有参数的任务的介绍前面我们使用线程执行的任务是没有参数的,假如我们使用线程执行的任务带有参数,如何给函数传参呢?Thread类执行任务并给任务传参数有两种方式:args 表示以…...

管理会计报告和财务报告的区别
财务会计报告是给投资人看的,可以反映公司总体的盈利能力。不过,我们回顾一下前面“第一天”里面提到的问题。如果你是公司的产品经理,目前有三个产品在你的管辖范围内。上级给你一笔新的资金,这笔资金应该投到哪个产品上…...
华为OD机试 - 最左侧冗余覆盖子串(Python) | 机试题算法思路 【2023】
最近更新的博客 华为OD机试 - 自动曝光(Python) | 机试题算法思路 【2023】 华为OD机试 - 双十一(Python) | 机试题算法思路 【2023】 华为OD机试 - 删除最少字符(Python) | 机试题算法思路 【2023-02】 华为OD机试 - Excel 单元格数值统计(Python) | 机试题算法思路 …...

【Opencv 系列】第1章 图像基础
通过本套课程,可以学到: 1.opencv的基本操作 2.两个案例,目标追踪&人脸识别 对重点内容,我会提示,包括我再准备这套课程过程中遇到的坑点! 最后代码我会放到git上,章节顺序一致:https://github.com/justinge/opencv_tutorial.git 系列文章目录 第1章 Opencv 图像基础 和 …...
创建和销毁对象——遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。比如用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里。还有超过20个的可选域:总脂肪量、饱和脂…...

【c++学习】入门c++(中)
目录一. 前言二. 函数重载1. 概念2.函数名修饰规则三 .引用(&)1. 概念2. 引用特性3.应用1.做参数2. 做返回值3. 传值、传引用效率比较4.引用和指针的区别四 . 结语一. 前言 小伙伴们大家好,今天我们继续学习c入门知识,今天的…...

论文阅读_AlphaGo_Zero
论文信息 name_en: Mastering the game of Go without human knowledge name_ch: 在没有人类知识的情况下掌握围棋游戏 paper_addr: http://www.nature.com/articles/nature24270 doi: 10.1038/nature24270 date_publish: 2017-10-01 tags: [‘深度学习’,‘强化学习’] if: 6…...
一文教你用Python创建自己的装饰器
python装饰器在平常的python编程中用到的还是很多的,在本篇文章中我们先来介绍一下python中最常使用的staticmethod装饰器的使用。 目录一、staticmethod二、自定义装饰器python类实现装饰器python函数嵌套实现装饰器多个装饰器调用三、带参数的装饰器一、staticmet…...
华为OD机试 - 任务总执行时长(JS)
任务总执行时长 题目 任务编排服务负责对任务进行组合调度。参与编排的任务又两种类型,其中一种执行时长为taskA,另一种执行时长为taskB。任务一旦开始执行不能被打断,且任务可连续执行。服务每次可以编排num个任务。 请编写一个方法,生成每次编排后的任务所有可能的总执…...

pytorch离线快速安装
1.pytorch官网查看cuda版本对应的torch和torchvisionde 版本(ncvv -V,nvidia-sim查看cuda对应的版本) 2.离线下载对应版本,网址https://download.pytorch.org/whl/torch_stable.html 我下载的: cu113/torch-1.12.0%2Bcu113-cp37-cp37m-win_…...
华为OD机试 - 数组合并(JS)
数组合并 题目 现在有多组整数数组,需要将他们合并成一个新的数组。 合并规则,从每个数组里按顺序取出固定长度的内容合并到新的数组中, 取完的内容会删除掉, 如果该行不足固定长度或者已经为空, 则直接取出剩余部分的内容放到新的数组中,继续下一行。 如样例1,获得长度3,先遍…...

不要让GPT成为你通向“学业作弊”的捷径——使用GPT检测工具来帮助你保持正确的方向
不要让GPT成为你通向“学业作弊”的捷径——使用GPT检测工具来帮助你保持正确的方向 最近,多所美国高校以及香港大学等都明确禁止在校使用ChatGPT等智能文本生成工具。GPT(Generative Pre-trained Transformer)是一种自然语言处理技术&#x…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...