Linux 网络套接字解析:实现网络通信
Aomnitrix 2024-08-29 13:07:03 阅读 91
目录
一.网络基础1.协议2.OSI与TCP/IP模型3.网络通信流程4.IP与Mac地址
二.网络编程套接字1.端口号2.网络字节序3.tcp、udp协议4.socket编程5.sockaddr结构解析6.实现Udp_socket7.实现Windows与Linux通信8.Linux下远程执行指令9.实现tcp_socket10.守护进程
一.网络基础
1.协议
计算机相关的硬件软件很多,这时就需要制定一个标准保障数据的传输,所以网络协议就是是计算机网络中通信双方遵循的约定。本质上来说:协议是通过结构体表达出来的特定的双方都认识的结构体对象。
2.OSI与TCP/IP模型
为了更好的将网络维护起来,OSI将网络模型分成了七层的理论模型,TCP/IP为互联网的实际模型通常为五层(四层)。
TCP/IP模型:
物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念数据链路层:负责处理硬件层面的通信细节,包括如何在本地网络上发送和接收数据。它涵盖了物理层和数据链路层的功能,管理数据帧的传输、错误检测、以及链路控制。网络层:主要负责数据包的路由和转发,确保数据能够在不同网络之间传输。这一层定义了IP地址的结构和数据包的传输路径。IP协议(IPv4和IPv6)是这一层的核心协议。传输层:提供端到端的数据传输服务,确保数据的完整性和顺序性。传输层负责数据的分段、传输、重组,并根据需要提供错误检测和流量控制。TCP和UDP协议在这一层运行,分别提供可靠的连接型和高效的无连接型数据传输服务。应用层:直接为用户提供网络服务,负责数据的格式化、加密以及应用程序之间的通信。这一层涵盖了OSI模型的应用层、表示层和会话层功能,是用户与网络交互的接口。
网络协议栈示意图:
3.网络通信流程
两主机通过TCP/IP通信过程所示
发送数据在不断的对数据加上报头,发送数据即封装的过程,接受数据是在不断的解包和分用的过程。分用:决定将自己的有效在和交付给上层的哪个协议的能力(就是分用)。传输层的叫数据段,网络层的叫叫数据报,数据链路层叫数据帧,通信的的过程本质就是不断封装和解包的过程,报文 = 报头 + 有效载荷。
局域网通信原理是基于碰撞域和碰撞检测的,交换机会划分碰撞域,局域网可以看成多台主机所共享的临界资源,所以是要保证互斥的。
4.IP与Mac地址
在计算机网络中,IP地址和MAC地址是两种用于标识设备的重要地址类型。
IP地址:
IP地址(是分配给网络中每个设备的逻辑地址,用于在网络层进行通信。IP地址有两种版本:IPv4和IPv6。
IPv4地址: 由32位二进制数构成,通常表示为四个十进制数(例如,192.168.1.1),每个数值代表8位(一个字节)。IPv6地址: 由于IPv4地址枯竭问题,IPv6应运而生,它使用128位地址空间,表示为8组16进制数(例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334)。
Mac地址:
MAC地址网络接口卡(网卡)的唯一标识符,存在于数据链路层。每个网络设备出厂时都会分配一个唯一的MAC地址,通常由硬件制造商确定。MAC地址由48位二进制数构成,6字节
MAC地址用于在同一局域网(LAN)内的设备之间进行通信。因为它是硬件地址,所以在设备移动到不同网络时保持不变。交换机等网络设备利用MAC地址来学习和识别局域网中设备的位置,并相应地转发数据帧。
在网络通信中,当一个设备需要与另一个设备通信时,通常需要知道对方的IP地址。然而,数据帧(数据链路层)的实际传输依赖于MAC地址。这时,ARP(地址解析协议)发挥作用。
二.网络编程套接字
1.端口号
端口号为2字节16位的整数,范围为在0到65535之间,它与IP地址一起构成一个套接字(socket),唯一标识网络中的某个服务。用来标识唯一进程。
那么端口号和进程pid有什么区别呢?,端口号是用来标识网络服务需要用到的进程,不是所有进程都要通信,但是所有进程都必须有pid标识自己。一个端口号可以被多个进程绑定,一个端口号不能绑定多个进程,在公网上:IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程 ,所以用IP和端口号可以标识全网唯一的一个进程。
2.网络字节序
在计算机网络中,数据传输可能会在不同计算机系统上。由于不同系统可能使用不同的字节序),为了确保数据在网络中传输时的正确性,需要统一使用一种标准的字节序。所以诞生了网络字节序:TCP/IP协议规定网络数据流应该采用大端字节序。所以如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
接着主机序列转网络序列的库函数应运而生:
<code>#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代表主机序列转网络序列,n代表网络字节序列转主机序列,其中如果机器是小端字节序会做相应的转换。
3.tcp、udp协议
在Linux网络编程中,TCP(传输控制协议)和UDP(用户数据报协议)是两种最常用的传输层协议。
UDP协议(用户数据报协议):
无连接:UDP是无连接的协议。数据传输之前不需要建立连接,因此可以更快地发送数据,但不保证数据的可靠性。不可靠传输:UDP不保证数据包的到达顺序,也不提供数据包的确认或重传机制。如果数据包丢失,UDP不会重发。面向数据报:在计算机网络中,**数据报(Datagram)**是指独立传输的、具有独立完整意义的数据单元。传输层协议
TCP协议(传输控制协议):
有连接:TCP是一种面向连接的协议。在数据传输之前,客户端和服务器需要通过三次握手建立连接,确保双方准备好进行通信。面向字节流:TCP中,数据被看作是一个连续的字节流。发送方可以将任意数量的字节发送到接收方,而接收方会以流的形式接收到这些字节。这意味着发送方发送的每一段数据不会被当作一个独立的数据单元,TCP不会将数据拆分成数据报。接收方接收到的数据流是发送方数据流的一个完整视图。可靠传输:TCP提供可靠的数据传输,通过数据包的确认和重传机制来确保数据完整无误。数据包的顺序也会被正确维护。传输层协议
4.socket编程
socket是一种通信机制socket 是应用层与传输层之间的一个接口,通过它,应用程序可以利用底层网络协议来发送和接收数据。网络协议栈中的Socket通常被用来实现TCP/IP协议通信。
socket编程系列函数:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket用来创建一个套接字:
domain:指定协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通信)。type:指定Socket类型,如 SOCK_STREAM(流式套接字,TCP)、SOCK_DGRAM(数据报套接字,UDP)。protocol:指定协议,一般设置为 0,让系统选择合适的协议。成功时返回一个Socket描述符(非负整数),失败时返回 -1 并设置 errno。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind函数在网络编程中用于将一个套接字(socket)绑定到一个特定的地址和端口(struct sockaddr结构体)。
sockfd:socket函数返回的Socket描述符。addr:指向struct sockaddr结构的指针,其中包含要绑定的地址信息。addrlen:addr结构的大小。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen 函数在网络编程中用于将一个已经绑定到地址和端口的套接字转换为监听状态,以便接受客户端的连接请求。这个函数主要用于服务器端。
sockfd:socket函数返回的Socket描述符。backlog:指定连接请求队列的最大长度。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept 函数在网络编程中用于接受客户端的连接请求。它主要用于服务器端,在一个已经处于监听状态的套接字上调用,用于从等待队列中提取一个连接请求,并返回一个新的套接字描述符用于与客户端进行数据交换。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数在网络编程中用于发起与远程主机的连接。它主要用于客户端程序,试图连接到指定的服务器地址和端口。
addr:说明:指向一个 struct sockaddr 结构体的指针,包含了远程主机的地址和端口信息。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom 函数在网络编程中用于从一个套接字接收数据,它特别适用于面向数据报的协议(如 UDP),但也可以用于其他协议。
sockfd:由 socket 函数创建的套接字描述符。这个套接字应该是用来接收数据的,并且可以是任何支持数据接收的协议(如 UDP)。buf:指向一个缓冲区的指针,用于存储接收到的数据。这个缓冲区应该足够大,以容纳预期的数据量。len:缓冲区 buf 的大小(以字节为单位)。接收到的数据量不能超过这个大小flags:接收操作的标志。通常使用 0,但也可以使用以下标志之一:MSG_OOB:接收带外数据(如果有)。MSG_PEEK:窥视数据,不从队列中移除。MSG_DONTWAIT:非阻塞模式(适用于非阻塞套接字)。src_addr:指向一个 struct sockaddr 结构体的指针,用于存储数据源的地址信息。对于 IPv4 通信,通常使用 struct sockaddr_in;对于 IPv6 通信,通常使用 struct sockaddr_in6。这个参数可以为 NULL,如果不需要源地址信息。addrlen:指向 socklen_t 类型变量的指针,表示 src_addr 结构体的大小。在调用前应设置为 src_addr 的大小;调用后,它将包含实际的地址长度。可以为 NULL,如果不需要源地址信息。
5.sockaddr结构解析
在网络编程中,sockaddr 结构体是用于表示网络地址的基础结构体。常见变体有sockaddr_in 和 sockaddr_un 的详细解析:
sockaddr 结构:是一个通用的地址结构体,用于表示网络地址。具体地址信息存储在其派生结构体中。sockaddr_in:用于表示 IPv4 地址,包含地址族、端口号和 IPv4 地址。适用于使用 TCP/UDP 协议的通信。sockaddr_un:用于表示UNIX 域套接字的本地通信地址,包含地址族和路径名。适用于本地进程间通信。
套接字分为三种:域间套接字(同一机器内)、原始套接字(网络工具)、网络套接字(用户间网络通信)
6.实现Udp_socket
首先我们分别要实现客户端和服务器逻辑,让服务器接受客户端的数据,并且返回给客户端。
首先我们创建服务器类:
<code>class UdpServer
{ -- -->
private:
int _socketid;//网络文件描述符
string _ip;//字符串形式ip地址
uint16_t _port;//服务器进程的端口号
};
构造函数:
UdpServer(uint16_t port = default_port, string ip = default_ip)
:_socketid(0)
,_ip(ip)
,_port(port)
{ }
绑定ip缺省为0.0.0.0代表允许接收来自任何IP地址的连接请求
创建套接字和绑定sockaddr_in结构体:
void Init()
{ //1.创建udp socket
_socketid = socket(AF_INET,SOCK_DGRAM,0);
if(_socketid <0)
{
exit(Socket_err);
}
//2.bind
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr_in _obj;
bzero(&_obj,sizeof(_obj));
_obj.sin_family = AF_INET;
_obj.sin_port = htons(_port);
_obj.sin_addr.s_addr = inet_addr(_ip.c_str());
int ret = bind(_socketid,(struct sockaddr*)&_obj,sizeof(_obj));
if(ret<0)
{
exit(Bind_err);
}
}
其中AF_INET代表IPV4协议,SOCK_DGRAM面向数据报的套接字,htons用于将主机字节序列的端口号转为网络序列,inet_addr将char*类型转换成in_addr_t类型,bind将套接字与存储好协议和ip与端口号的struct sockaddr_in绑定起来。bzero将指定内存块清空,常用于使用之前初始化结构体
接着进行客户端数据收发逻辑:
void run()
{
char Buffer[1024];
string temp;
while(true)
{
//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
//struct sockaddr *src_addr, socklen_t *addrlen);
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t Re_ret = recvfrom(_socketid,Buffer,sizeof(Buffer)-1,0,//接收
(struct sockaddr*)&client,&len);
if(Re_ret<0)
{
lg(Fatal,"bind create is error : %d",Re_ret);
continue;
}
Buffer[Re_ret] = 0;//看成字符串
temp = Buffer;
cout<<temp<<endl;
//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
//const struct sockaddr *dest_addr, socklen_t addrlen);
sendto(_socketid,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送
}
}
客户端逻辑:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " serverip serverport"<< endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string Ser_ip = argv[1];
uint16_t Ser_proc = stoi(argv[2]);
//1.创建struct sockaddr
struct sockaddr_in Server;
bzero(&Server,sizeof(Server));
Server.sin_family=AF_INET;
Server.sin_port = htons(Ser_proc);
Server.sin_addr.s_addr= inet_addr(Ser_ip.c_str());
int _sockeid = socket(AF_INET,SOCK_DGRAM,0);
if(_sockeid <0)
{
cout<<"Client is error "<<endl;
exit(1);
}
char Buffer[1024];
string message;
socklen_t len = sizeof(Server);
while(true)
{
cout<<"Please enter :";
getline(cin,message);
sendto(_sockeid,message.c_str(),sizeof(message)-1,0,(struct sockaddr*)&Server,len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(_sockeid, Buffer, sizeof(Buffer)-1, 0, (struct sockaddr*)&temp, &len);//接受&
if(s > 0)
{
Buffer[s] = 0;
cout << Buffer << endl;
}
}
close(_sockeid);
return 0;
}
客户端也是需要bind的但是是由OS在客户端发送数据的时候自动做。不需要显示调用bind首先客户端想要与服务器通信,要知道服务器的ip和端口号,这里用到了可变参数,让用户的运行的时候告诉进程服务器的ip和端口号。之后的逻辑乏善可陈,即是发送和接受数据。
我们再设置服务器端口号时,要知道0到1023:为系统内定的端口号,一般都要有固定的。
如此以来我们就封装出了udpsocket实现简单的cs通信。
7.实现Windows与Linux通信
将Linux机器作为服务器,代码逻辑与上相同:
<code>#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
Log lg;
enum{ -- -->
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip)
:sockfd_(0),
port_(port),
ip_(ip),
isrunning_(false)
{ }
void Init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
if(sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
// local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void Run() // 对代码进行分层
{
isrunning_ = true;
char inbuffer[size];
string temp;
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = 0;//看成字符串
temp = inbuffer;
cout<<temp<<endl;
//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
//const struct sockaddr *dest_addr, socklen_t addrlen);
sendto(sockfd_,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
bool isrunning_;
};
Windows下vs2022代码:
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <Windows.h>
#include<iostream>
#include<string>
#pragma comment(lib,"ws2_32.lib") //引入库文件
int main()
{
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
printf("WSAStartup failed\n");
return -1;
}
//建立一个udp的socket
SOCKET socked = socket(AF_INET, SOCK_DGRAM, 0);
if (socked == INVALID_SOCKET)
{
printf("create socket failed\n");
return -1;
}
int port = 8080;
std::string ip = "";//服务器ip地址
//创建结构体
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.S_un.S_addr = inet_addr(ip.c_str());
std::string info;
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
//收发数据
while (true) {
std::cout << "Please enter:";
std::getline(std::cin, info);
//发送数据
int n = sendto(socked, info.c_str(), info.size(), 0, (SOCKADDR*)&addr, sizeof(SOCKADDR));
if (n == 0)
{
printf("send failed\n");
return -1;
}
sockaddr_in t = { 0 };
int len = sizeof(sockaddr_in);
// 接收数据
n = recvfrom(socked, buffer, sizeof(buffer) - 1, 0, (SOCKADDR*)&t, &len);
buffer[n] = 0;
std::cout << buffer << std::endl;
memset(buffer, 0, sizeof(buffer));
}
//关闭SOCKET连接
closesocket(socked);
//清理网络环境
WSACleanup();
return 0;
}
即可完成通信
8.Linux下远程执行指令
popen 函数允许程序通过一个文件流与外部命令进行交互:
<code>FILE *popen(const char *command, const char *type);
command:一个指向字符串的指针,表示要执行的命令。这可以是任何可以在命令行执行的命令,包括路径、可执行文件名以及命令的参数。type:一个指向字符串的指针,用于指定文件流的类型,可以是 “r” 或 “w”:“r”:表示读取(即从命令的标准输出读取数据)。“w”:表示写入(即将数据写入到命令的标准输入)。成功时,返回一个指向 FILE 的指针,该指针可以用于读取或写入外部命令的输入输出。
也就是客户端向服务器发送数据后,加一层处理,将数据转换成指令并将结果读取并打印出来:
string ExcuteCommand(const string &cmd)
{ -- -->
cout << "get a request cmd: " << cmd << endl;
FILE *fp = popen(cmd.c_str(), "r");
if(nullptr == fp)
{
perror("popen");
return "error";
}
string ret;
char buffer[4096];
while(true)
{
char *tmp = fgets(buffer, sizeof(buffer), fp);
if(tmp == nullptr) break;
ret += buffer;
}
pclose(fp);
return ret;
}
当然用Windows也是可以的:
其实我们的xshell原理就类似:
我们是客户端访问远端服务器后输入数据被解析成指令,服务器再将结果返回给我们。
9.实现tcp_socket
系列函数解析:
listen 函数在网络编程中用于将一个套接字从“主动”状态切换到“监听”状态,,通常在调用 socket 函数创建套接字并用 bind 绑定到地址和端口之后使用。
<code>#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog:指定套接字的最大连接队列长度,即在服务器处理新连接之前,内核中允许排队的未决连接数。这个数值决定了在连接请求被接受之前,内核可以缓存多少个连接请求。
accept 函数是网络编程中的一个重要函数,用于从已经处于监听状态的套接字中接受一个连接请求,并返回一个新的套接字描述符,该描述符用于与客户端进行通信
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
成功时,返回一个新的套接字描述符,该描述符用于与客户端进行通信。三次握手完成后, 服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
connect 函数用于将一个套接字连接到一个远程主机上的指定地址。用于客户端连接服务器,连接成功后,客户端可以通过该套接字与服务器进行数据交换。
tcp通信也是全双工的,因为他的发送缓冲区和接收缓冲区是分开的,所以接收和发送是可以同时进行的。
我们先来实现简单的tcpsocket的cs通信:
服务器代码逻辑:
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
//#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;
const string defaultip = "0.0.0.0";
const int backlog = 5;
extern Log lg;
enum
{ -- -->
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer
{
public:
TcpServer(const uint16_t &port, const string &ip = defaultip)
: _port(port), _ip(ip)
{
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in _obj;
bzero(&_obj, sizeof(_obj));
_obj.sin_family = AF_INET;
_obj.sin_port = htons(_port);
_obj.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(_obj);
if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0)
{
lg(Fatal, "Server bind is error %s", strerror(errno));
exit(BindError);
}
lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);
// 监听
if (listen(_listensock, backlog) < 0)
{
lg(Fatal, "Server listen is error %s", strerror(errno));
exit(ListenError);
}
lg(Info, "Server listen is succes ");
}
void _test(int _sockefd, uint16_t &port, string &clientip)
{
char buffer[4096];
while (true)
{
ssize_t n = read(_sockefd, buffer, sizeof(buffer) - 1); // 留一个位置给 '\0'
if (n > 0)
{
buffer[n] = 0; // 添加终止符,防止溢出
cout << "client say# " << buffer << endl;
string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(_sockefd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close _sockefd: %d", clientip.c_str(), port, _sockefd);
break;
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", _sockefd,
clientip.c_str(), port);
break;
}
}
}
void Start()
{
lg(Info, "tcpServer is running....");
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);
if (_sockefd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
uint16_t clientport = ntohs(client.sin_port);
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);
_test(_sockefd, client.sin_port, _ip);
close(_sockefd);
}
}
~TcpServer() { }
private:
int _listensock;
uint16_t _port;
string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;
void Usage(const string &proc)
{
cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
lg(Fatal, "Client connect is error %s", strerror(errno));
}
while (true)
{
string message;
cout << "Please Enter# ";
getline(cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
cerr << "write error..." << endl;
}
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
cout << inbuffer << endl;
}
}
close(sockfd);
}
return 0;
}
运行起来既实现
可是当前的代码是单进程的,如果有两台主机同时向服务器发送数据,服务器只能处理先连接上的,那我们服务器能同时处理多个客户端的数据该怎么办呢?
我们可以将代码逻辑改成多进程版本的:
核心代码:
<code>pid_t id=fork();
if(id==0)
{ -- -->
close(_listensock);
if(fork()>0)exit(0);
_test(_sockefd, client.sin_port, _ip);
close(_sockefd);
exit(0);
}
close(_sockefd);
pid_t w_id=waitpid(id,nullptr,0);
这时就完成了多进程的代码逻辑。
其中:
<code>if(fork()>0)exit(0);
目的是在子进程中创建孙子进程让孙子进程执行_test代码,这样在执行_test的时候,子进程已经被父进程回收,所以可以继续创建子进程完成逻辑,实现多主机可以同时访问服务器的功能。使用多线程的话也是类似的逻辑,但是创建线程的成本比进程低的多,这里就不演示了。
接下来我们编写一个基于线程池处理汉译英功能的服务器。首先直接将我们之前编写的单例模式的线程池拿过来:
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
struct ThreadInfo
{ -- -->
pthread_t tid;
std::string name;
};
static const int defalutnum = 10;
template <class T>
class ThreadPool
{
public:
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
bool IsQueueEmpty()
{
return tasks_.empty();
}
std::string GetThreadName(pthread_t tid)
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
static void *HandlerTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t();
}
}
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
}
}
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // ???
{
pthread_mutex_lock(&lock_);
if (nullptr == tp_)
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>();
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
std::vector<ThreadInfo> threads_;
std::queue<T> tasks_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *tp_;
static pthread_mutex_t lock_;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
服务器代码逻辑:
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;
const string defaultip = "0.0.0.0";
const int backlog = 6;
extern Log lg;
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
{ }
public:
int sockfd;
string clientip;
uint16_t clientport;
TcpServer *tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port, const string &ip = defaultip)
: _port(port), _ip(ip)
{
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in _obj;
bzero(&_obj, sizeof(_obj));
_obj.sin_family = AF_INET;
_obj.sin_port = htons(_port);
_obj.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(_obj);
if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0)
{
lg(Fatal, "Server bind is error %s", strerror(errno));
exit(BindError);
}
lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);
// 监听
if (listen(_listensock, backlog) < 0)
{
lg(Fatal, "Server listen is error %s", strerror(errno));
exit(ListenError);
}
lg(Info, "Server listen is succes ");
void Start()
{
ThreadPool<Task>::GetInstance()->Start();
lg(Info, "tcpServer is running....");
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);
if (_sockefd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
uint16_t clientport = ntohs(client.sin_port);
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);
Task t(_sockefd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
~TcpServer() { }
private:
int _listensock;
uint16_t _port;
string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;
void Usage(const string &proc)
{
cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
lg(Fatal, "Client connect is error %s", strerror(errno));
return 1;
}
while (true)
{
string message;
cout << "Please Enter# ";
getline(cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
cerr << "write error..." << endl;
break;
}
else if (n == 0) {
break;
}
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
cout << inbuffer << endl;
}
else if(n == 0) {
break;
}
}
close(sockfd);
}
return 0;
}
英译汉搜索逻辑:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
Log lg;
const string dictname = "./dict.txt";
const string sep = ":";
//yellow:黄色...
static bool Split(string &s, string *part1, string *part2)
{
auto pos = s.find(sep);
if(pos == string::npos) return false;
*part1 = s.substr(0, pos);
*part2 = s.substr(pos+1);
return true;
}
class Init
{
public:
Init()
{
ifstream in(dictname);
if(!in.is_open())
{
lg(Fatal, "ifstream open %s error", dictname.c_str());
exit(1);
}
string line;
while(getline(in, line))
{
string part1, part2;
Split(line, &part1, &part2);
dict.insert({ part1, part2});
}
in.close();
}
string translation(const string &key)
{
auto iter = dict.find(key);
if(iter == dict.end()) return "Unknow";
else return iter->second;
}
private:
unordered_map<string, string> dict;
};
任务代码逻辑:
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
extern Log lg;
Init init;
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
: sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
{
}
Task()
{
}
void run()
{
char buffer[4096];
while(1) {
ssize_t n = read(sockfd_, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client key# " << buffer << std::endl;
std::string echo_string = init.translation(buffer);
n = write(sockfd_, echo_string.c_str(), echo_string.size());
if(n < 0)
{
lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
}
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
break;
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
break;
}
}
close(sockfd_);
}
~Task()
{
}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};
10.守护进程
我们之前学习过前台进程和后台进程,在命令行中前台进程是必须要一直存在的,能接受键盘信号的就是前台进程。所以说谁拥有键盘文件谁就是前台进程,一个会话可以有多个后台进程但只能有一个前台进程。&符号就是用来将程序设置到后台运行的:
jobs指令可以查看后台任务:
fg加任务号可以将后台任务提到前台:
ctrl Z可将将其暂停并放回后台:
bg加任务号可将后台任务在跑起来:
每次登录xshell就会建立一个会话,所以有会话id的概念,TTY为控制终端,一个进程组只有一个进程的pid和pgid相同,组长就是进程组的第一个。
我们可以观察到终端是 ?这表明它没有关联到任何终端。
守护进程的父进程 ID(PPID)是 1,这表明它们可能是由 init启动的。守护进程的会话 ID 通常与其进程组 ID 相同,表示它们属于同一会话
函数setsid:
<code>#include <unistd.h>
pid_t setsid(void);
setsid 函数是用于创建新的会话的系统调用。它的主要作用是将当前进程创建为新的会话的领导者,从而实现与当前会话的断开,通常用于守护进程的创建。也就是说守护进程是自成会话的进程,需要注意的是setsid的调用进程不能是进程组组长,所以我们需要fork子进程执行这个函数。守护进程也是孤儿进程
代码逻辑实现:
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{ -- -->
// 1. 忽略其他异常信号
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2. 将自己变成独立的会话
if (fork() > 0)
exit(0);
setsid();
// 3. 更改当前调用进程的工作目录
if (!cwd.empty())
chdir(cwd.c_str());
// 4. 标准输入,标准输出,标准错误重定向至/dev/null
int fd = open(nullfile.c_str(), O_RDWR);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
逻辑解析:
定义了一个常量 nullfile,指定了 /dev/null,任何写入到它的数据都会被丢弃,读取则返回 EOF。忽略异常信号这些信号处理设置确保了守护进程不会因为这些信号而被意外终止。fork():创建一个子进程。父进程退出,子进程继续执行。这让子进程脱离原有的会话。setsid():创建一个新的会话,并将调用进程设置为该会话的会话组长。这样,进程就不再与终端相关联,可以在后台运行。chdir(cwd.c_str());:如果提供了工作目录路径,则更改进程的工作目录。通常,守护进程会将工作目录更改为根目录。open(nullfile.c_str(), O_RDWR);:打开 /dev/null 设备文件。dup2(fd, 0);、dup2(fd, 1);、dup2(fd, 2);:将标准输入(0)、标准输出(1)、标准错误(2)重定向到 /dev/null。这样,守护进程的输入输出不会干扰终端,也不会产生任何不必要的输出。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。