目录
- 认识端口号
- 认识传输层协议TCP/UDP
- 网络字节序
- socket编程接口
- 实现简单的UDP网络程序
- 实现远程执行服务器shell指令
- Windows套接字编写
- UDP实现一个简单的聊天室
- 实现简单的TCP网络程序
- TCP实现一个中英互译程序
- 守护进程原理
认识端口号
在进行网络通信的时候是不是我们两台机器在通信呢?是两个进程在通信,以前是单主机的两个进程在通信,现在是跨主机的两个进程在通信。之前讲过,进程间通信的前提是:让两个进程先看到同一份资源,这份资源是谁?是网络
在网络协议栈中的下三层,主要解决的是:数据安全可靠的送到远端机器。用户使用应用层软件完成数据发送和接收的,所以要软件是需要启动起来变为进程的
当数据从传输层发送到应用层的时候,传输层怎么知道要将报文交给哪个应用?
端口号无论对于client(客户端)或server(服务器),都能标识主机上唯一的一个进程。
在公网上:IP地址能表示唯一的一台主机。端口号port用来标识主机上的唯一的一个进程
IP+Port=标识全网唯一的一个进程
有人就要问了,进程已经有pid了,进程pid不是可以标识进程的唯一性吗,为什么还要有port呢?
首先要说明一点:从技术角度,操作系统是可以这样设计的。但是没这样设计有几点理由
1.每启动一次进程pid是会改变的,而对于服务器来说它的端口是不能随便改变的,因为你使用的客户端是厂家程序员内置好的,需要客户端每次都能找到服务器进程,所以服务器的唯一性(IP+Port)不能做任何改变。
2.不是所有进程都要提供网络服务,但是所有进程都需要pid。所以网络单独设计更好
3.系统是系统,网络是网络,单独设置–能是系统和网络解耦。如果系统部分一改,那么你的网络部分势必也会受影响,解耦就能很好解决这样的问题
端口号是传输层协议的内容
一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?从上图哈希表的key和value不难发现一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定.
认识传输层协议TCP/UDP
TCP协议:Transmission Control Protocol传输层控制协议
1.传输层协议2.有连接3.可靠传输4.面向字节流
UDP协议:User Datagram Protocol用户数据报协议
1.传输层协议2.无连接3.不可靠传输4.面向数据报
什么是有连接?tcp通信的时候,需要客户端向服务器发起连接,建立成功之后(如何建立连接呢?三次握手,后面会讲),才能正式的发送数据。就像打电话的时候,需要噔噔噔几声接通了才能和对方讲话。
什么是可靠传输?有连接也是可靠传输的一种。tcp也会检测底层,如果发送失败了,会自动重传,所以应用层感觉不到。还会检测接收方的接收能力。会按需到达、流量控制、预测控制等(后面会讲)
什么是不可靠?发送的数据报丢包了或者乱续了,udp 完全不管
面向数据包?发出去的报文有明显边界,发出去一个报文,就是一个报文,发一个对方就得收一个,发一个对方就得收一个。udp就如发送邮件,邮件发送到对方后,对方要么不收邮件,收邮件就是一个完整的邮件,不会是半个邮件,发邮件的时候也不需要给对方打电话建立连接,只需要放到它的邮箱
什么是数据流?像流动的水一样,无边界,发送的时候可以断断续续的发送,接收的时候需要自己将它们分开
可靠和不可靠是中性词,无褒贬之分。可靠就必然有成本—维护+编码策略。不可靠就很简单–维护+使用
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分(大小端的理解关键就在于把数据和存储空间分开,然后再把数据放到存储空间里。这个放法不同,那么就有了大小端的区别)
为什么叫命名大小端呢?其实是来源于格列佛游记里面的一则故事,关于一个人吃鸡蛋,鸡蛋有大端和小端,有人说吃鸡蛋先从大端开始吃,有人说吃鸡蛋先从小端开始吃,大家都有争执,而且很难形成共识。所以呢就就是存在了两种同时存在的吃鸡蛋的方法。在我们计算机领域呢,那么把数据存在我们的呃高权值位层的高地址,还是高权值位层低地址,它本身也是没有标准答案的。
因为网络的发展比计算机的发展要晚个几十年,所以在网络出现之前,早就有计算机就意味着早就需要解决数据如何在内存当中存的问题。所以在那个年代,在硬件和软件上的存储方案,因为没有充分的理由去说服对方,所以有的认为小端好,有的认为大端好。
这样就会产生一个问题:因为跨网络发送消息的时候我们两个的主机并不知道对方是大端还是小端,所以我发给你的时候你也并不知道按照小端的形式解析还是按照大端的形式解析。有人就提出了一个解决方案,你接受我的消息的时候,看我数据包的协议就可以了,但是这样仍然有问题,因为不知道按大端还是小端解析这个协议
最终的解决方案:不管你机器是大端机器还是小端机器,往网络里发送就必须是大端字节序的。大端机器接收的时候可以直接接收,小端机器只用接收后将大端转为小端即可。在硬件层面:发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存(低地址是放低权值位,还是高权值位就取决于你是大端还是小端)
如果大小端自己转的话太麻烦了,系统为我们提供了这样的接口
端口号转换
h就代表主机host的意思,n就代表网络net的意思
IP是给用户看的,所以使用字符串表示,但机器不看字符串,看的是in_addr类型(底层就是整形),所以也存在IP的转换
字符串转in_addr函数
第一个函数,第一个参数:输入型参数。第二个参数:输出型参数
第二个函数,参数:输入型参数,要转的字符串。返回值,转后的in_addr的子类型:in_addr_t类型
第三个函数,第一个参数:域,协议家族,填AF_INET。第二个参数:输入型参数。第三个参数:输出型参数填in_addr类型
in_addr转字符串函数
第一个函数很好理解。第二个函数:第一个参数,域,协议家族,填AF_INET。第二个参数,输入型参数,填in_addr类型,第三个参数输出型参数,填接收的字符串,第四个参数,你提供的字符串的长度
socket编程接口
常见的socket编程接口有
我们可以看到都有一个参数socket,对网络操作都是对socket/sockfd进行操作。还可以看到都有一个struct sockaddr* 的参数,关于这个参数我想给大家讲一下,套接字不仅仅只有一种,有三种套接字,有网络套接字,原始套接字(对网络层操作的套接字、主要用来编写网络工具,检查计算机是否连通,网络抓包等),unix域间套接字(本地通信的套接字)。这样的话就需要设计三种套接字接口,但设计者不想设计三套,只设计了一套接口,这样就可以通过不同的参数,解决所有网络或者其它场景下的通信问题
函数体内有会内置这样的逻辑:
if(struct sockaddr->type == struct sockaddr_in)
{
//执行网络套接字的逻辑
}
else if (struct sockaddr->type == struct sockaddr_un)
{
//执行域间套接字的逻辑
}
实现简单的UDP网络程序
class UdpServer
{
public:
private:
int _sockfd; //网络文件描述符
string _ip;
uint16_t _port;
};
我们之前说了对网络操作就是对文件进行操作,函数作用就是返回文件描述符,出错了就返回-1
第一个参数domain:域。填AF_UNIX/AF_LOCAL表示本地通信。填AF_INET表示网络通信
第二个参数type:套接字提供的服务。SOCK_STREAM表示tcp通信,SOCK_DGRAM表四UDP通信
第三个参数protocol:填0即可_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
函数作用给socket绑定IP地址和port端口号
第一个参数:输入型参数。第二个参数的类型,我们上面讲了,这个类型是用来设计接口的,而我们就直接用网络套接字类型
>
结构体中第一个字段
__SOCKADDR_COMMON (sa_)
会被宏替换成为sa_family_t sin_family
。表明该结构体自身的域(协议家族),填AF_INET
第二个字段,填要绑定的端口号。
第三个字段,填要绑定的IP地址。
第四个字段为填充字段,不用管
struct sockaddr_in local;
local.sin_addr.s_addr = inet_addr(_ip.c_str());//inet_addr将const char*转为32位的整数
local.sin_family = AF_INET;
local.sin_port = htons(_port);
bind(_sockfd, (const sockaddr*)&local, sizeof(local));
该文件我们之前写过,这里直接拿来用,如果没看过的,这里的Log.hpp完全可用打印printf来代替
Log.hpp文件
#include <iostream>
#include <string>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define fileName "log.txt"
//使用前需要创建log目录
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
void printOneFile(string logname, const string& logtxt)
{
logname = path + logname;
int fd = open(logname.c_str(), O_WRONLY | O_APPEND | O_CREAT, 0666);//open只会创建文件不会创建目录
if (fd < 0)
{
perror("open failed");
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const string& logtxt)
{
string filename = fileName;
filename += ".";
filename += leveltoString(level);
printOneFile(filename, logtxt);
}
void printLog(int level, const string& logtxt)
{
if (printMethod == Screen)
{
cout << logtxt << endl;
return;
}
else if (printMethod == Onefile)
{
printOneFile(fileName, logtxt);
return;
}
else if (printMethod == Classfile)
{
printClassFile(level, logtxt);
return;
}
}
const char* leveltoString(int level)
{
if (level == Info) return "Info";
else if (level == Debug) return "Debug";
else if (level == Error) return "Error";
else if (level == Fatal) return "Fatal";
else return "default";
}
void operator()(int level, const char* format, ...)
{
time_t t = time(nullptr);
struct tm* st = localtime(&t);
char leftbuffer[4096];
snprintf(leftbuffer, sizeof(leftbuffer), "year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d\n\
[%s]:",
st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec, leveltoString(level));
char rightbuffer[4096];
va_list start;
va_start(start, format);
vsnprintf(rightbuffer, sizeof(rightbuffer), format, start);
va_end(start);
char logtxt[4096 * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
string path;//路径与文件名解耦,最后将路径和文件粘合起来,再用open打开即可
};
udpserver.hpp文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
using namespace std;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
static const string defaultIp = "0.0.0.0";
static const uint16_t defaultPort = 8080;
class UdpServer
{
public:
UdpServer(uint16_t port = defaultPort, string ip = defaultIp)
:_ip(ip), _port(port)
{}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
lg(Fatal, "create socket failed: %s", strerror(errno));
exit(SOCKET_ERR);
}
struct sockaddr_in local;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_family = AF_INET;
local.sin_port = htons(_port);
int n = bind(_sockfd, (const sockaddr*)&local, sizeof(local));
if (n != 0)
{
lg(Fatal, "bind socket failed: %s", strerror(errno));
exit(BIND_ERR);
}
}
void run()
{
while (1)
{
lg(Debug, "udp server running");
sleep(1);
}
}
private:
int _sockfd; //网络文件描述符
string _ip;
uint16_t _port;
};
udpserver.cc文件
#include <iostream>
#include "udpserver.hpp"
int main(int argc, char* argv[])
{
uint16_t port = 8080;
string ip = "0.0.0.0";
if (argc > 1)
port = stoi(argv[1]);
if (argc > 2)
ip = argv[2];
UdpServer* us = new UdpServer(port, ip);
us->Init();
us->run();
return 0;
}
运行结果
因为一台电脑上不只有一个IP,真实的情况服务器能够接收从这些所有的IP地址来的信息,所以我们bind时,bindIP地址为0代表可以接收所有IP地址
函数作用收一个报文。
第一个参数输入型参数,第二个参数为输出型参数,为接收报文内容的缓冲区,第三个参数为缓冲区的大小,第四个参数为设置读取方式,填0为阻塞读取,第四个参数,为输出型参数,能够获得发送方的属性信息,第五个参数,为第四个参数的大小
函数作用发送一个报文。
第一个参数输入型参数,第二个参数为输入型参数,要发送内容的缓冲区,第三个参数缓冲区的大小,第四个参数设为0即可,第五个参数,要发送给谁,第六个参数,为第五个参数的大小
更新udpserver.hpp文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>
#include "Log.hpp"
using namespace std;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
static const string defaultIp = "0.0.0.0";
static const uint16_t defaultPort = 8080;
class UdpServer
{
using func_t = function<string(const string&)>;
public:
UdpServer(uint16_t port = defaultPort, const string& ip = defaultIp)
:_ip(ip), _port(port)
{}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
lg(Fatal, "create socket failed: %s", strerror(errno));
exit(SOCKET_ERR);
}
struct sockaddr_in local;
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY; //接收该电脑上所有的IP地址
local.sin_family = AF_INET;
local.sin_port = htons(_port);
int n = bind(_sockfd, (const sockaddr*)&local, sizeof(local));
if (n != 0)
{
lg(Fatal, "bind socket failed: %s", strerror(errno));
exit(BIND_ERR);
}
}
void run(func_t func)//对代码进行分层
{
while (1)
{
lg(Debug, "udp server running");
sleep(1);
char recvbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_sockfd, recvbuffer, sizeof(recvbuffer), 0, (struct sockaddr*)&peer, &len);
if (n < 0)
{
lg(Warning, "recvfrom message failed:%s", strerror(errno));
continue;
}
recvbuffer[n] = 0;
string info = recvbuffer;
string clientIp = inet_ntoa(peer.sin_addr);
uint16_t clientPort = ntohs(peer.sin_port);
lg(Info, "recv a client message:%s, clientIP:%s, clinetPort:%d", info.c_str(), clientIp.c_str(), clientPort);
//将info信息处理服务后再发送给对方
string handler = func(info);
sendto(_sockfd, handler.c_str(), handler.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
}
}
private:
int _sockfd; //网络文件描述符
string _ip; //任意地址bind 0
uint16_t _port; //表明服务器进程的端口号
};
更新udpserver.cc文件
#include <iostream>
#include "udpserver.hpp"
string func(const string& info)
{
//处理逻辑简写。。。
return info;
}
int main(int argc, char* argv[])
{
uint16_t port = 8080;
string ip = "0.0.0.0";
if (argc > 1)
port = stoi(argv[1]);
if (argc > 2)
ip = argv[2];
UdpServer* us = new UdpServer(port, ip);
us->Init();
us->run(func);
return 0;
}
服务器编写完成,下面编写客户端
问题:客户端的端口号是否需要绑定?肯定要绑定,之前说过了IP+Port才能在全网标识唯一性。但是不需要我们显式的(自己写代码)去绑定,为什么呢?因为如果显式的去绑定,你使用的进程不止这一个,如果你要绑定的和别的进程冲突了呢?那这个进程就跑不起来了,如你手机上有QQ有微信,有抖音,这些公司会去商量我用这个端口号你别用吗,肯定不会。所以我们也不要显式绑定,那什么时候绑定呢?当你第一次发送消息的时候,操作系统会自动帮你绑定,因为操作系统很清楚,那些端口号可用那些端口号不可用,这样就能确保不会冲突了。
因为udpserver.cc文件简单所以就不单独写头文件了
udpserver.cc文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"
//使用手册
void Usage(char* str)
{
printf("\n\t%s\tserverIp\tserverPort\n", str);
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
char buffer[1024];
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
while (1)
{
string message;
cout << "Enter#";
getline(cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&server, sizeof(server));
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, 0, 0);//不关心发送方的信息即可设为0
if (n > 0)
{
buffer[n] = 0;
cout << "server say:" << buffer << endl;
}
}
return 0;
}
127.0.0.1为本地换回地址,专门用于服务器代码测试,不经过物理层
运行结果
实现远程执行服务器shell指令
该函数的原理就是fork+pipe+exec*来执行命令,具体原理可查看往期文章
第一个参数:输入的shell指令的字符串。第二个参数:以r/w/a方式打开管道文件
返回值:返回管道文件的FILE * ,错误返回0
只用更新 服务器接收客户端发送的字符串 后的处理函数,即可满足客户端不同的服务需求。(这也是一种协议约定(双方都默许认可的),你发送这些shell指令,我就能给服务给你你想要的)
所以只用更新udpserver.cc文件
#include <iostream>
#include "udpserver.hpp"
string ExcuteCommand(const string& cmd)
{
FILE* fp = popen(cmd.c_str(), "r");
if (fp == 0)
{
lg(Fatal, "popen failed:%s", strerror(errno));
exit(1);
}
string ret;
char buffer[4096];
while (1)
{
char* s = fgets(buffer, sizeof(buffer) - 1, fp);
if (s == NULL) break;
ret += buffer;
}
pclose(fp);
return ret;
}
int main(int argc, char* argv[])
{
uint16_t port = 8080;
string ip = "0.0.0.0";
if (argc > 1)
port = stoi(argv[1]);
if (argc > 2)
ip = argv[2];
UdpServer* us = new UdpServer(port, ip);
us->Init();
us->run(ExcuteCommand);
return 0;
}
运行结果:
Windows套接字编写
大部分的操作系统是不一样的,但他们的网络协议栈是一样的,那么windows的套接字和linux的套接字接口是一样的吗?一样,因为你也必须要支持tcp/ip协议。所以上面的代码能在linux环境下跑也能在window环境下跑。我们设计让服务器在linux上跑,客户端在window上跑
在Windows上编写网络套接字有些许区别,区别如下,其它的代码可以一模一样
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <WinSock2.h>
#include <Windows.h>
#pragma comment(lib, "ws2_32.lib")
WSAData wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd))//初始化
{
cout << "error";
return 0;
}
//代码编写
//...
closesocket(sockfd);
WSACleanup();
我们使用vs2019编译器
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include<iostream>
#include <string>
#include <WinSock2.h>
#include <Windows.h>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
string serverIp = "192.168.214.128";
uint16_t serverPort = 8080;
int main()
{
WSAData wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd))//初始化
{
cout << "error";
return 0;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
string s;
while (1)
{
cout << "Enter#";
getline(cin, s);
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
server.sin_family = AF_INET;
sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr*)&server, sizeof(server));
char recvbuffer[4096];
int n = recvfrom(sockfd, recvbuffer, sizeof(recvbuffer) - 1, 0, 0, 0);
if (n < 0) break;
recvbuffer[n] = 0;
cout << recvbuffer;
}
closesocket(sockfd);
WSACleanup();
return 0;
}
运行结果
UDP实现一个简单的聊天室
功能:客户端进行聊天,我发一条消息,其它人都能收到。
我用两个不同的IP虚拟机进行测试
udpserver.hpp文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
static const string defaultIp = "0.0.0.0";
static const uint16_t defaultPort = 8080;
class UdpServer
{
using func_t = function<string(const string&)>;
public:
UdpServer(uint16_t port = defaultPort, const string& ip = defaultIp)
:_ip(ip), _port(port)
{}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
lg(Fatal, "create socket failed: %s", strerror(errno));
exit(SOCKET_ERR);
}
struct sockaddr_in local;
// local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY; //接收该电脑上所有的IP地址
local.sin_family = AF_INET;
local.sin_port = htons(_port);
int n = bind(_sockfd, (const sockaddr*)&local, sizeof(local));
if (n != 0)
{
lg(Fatal, "bind socket failed: %s", strerror(errno));
exit(BIND_ERR);
}
}
void CheckUser(const struct sockaddr_in& peer, const string& clientIp, uint16_t port)//检查用户是否登录,没登陆就添加他
{
if (!online_user.count(clientIp))
{
cout << "[clientIp:" << clientIp << ":" << "clientPort:" << port << "]add to online user" << endl;
online_user[clientIp] = peer;
}
}
void Broadcast(func_t func, const string& info, const string& clientIp, uint16_t port)//将消息广播给所有用户
{
for (auto& e : online_user)
{
string message = "clientIp:" + clientIp + ":" + info;
string handler = func(message);
sendto(_sockfd, handler.c_str(), handler.size(), 0, (struct sockaddr*)(&e.second), sizeof(e.second));
}
}
void run(func_t func)
{
while (1)
{
char recvbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_sockfd, recvbuffer, sizeof(recvbuffer), 0, (struct sockaddr*)&peer, &len);
if (n < 0)
{
lg(Warning, "recvfrom message failed:%s", strerror(errno));
continue;
}
recvbuffer[n] = 0;
string info = recvbuffer;
string clientIp = inet_ntoa(peer.sin_addr);
uint16_t clientPort = ntohs(peer.sin_port);
CheckUser(peer, clientIp, clientPort);
Broadcast(func, info, clientIp, clientPort);
}
}
private:
int _sockfd; //网络文件描述符
string _ip; //任意地址bind 0
uint16_t _port; //表明服务器进程的端口号
unordered_map<string, struct sockaddr_in> online_user;//<IP, 客户信息>
};
udpserver.cc文件
#include <iostream>
#include "udpserver.hpp"
string func(const string& str)
{
return str;
}
int main(int argc, char* argv[])
{
uint16_t port = 8080;
string ip = "0.0.0.0";
if (argc > 1)
port = stoi(argv[1]);
if (argc > 2)
ip = argv[2];
UdpServer* us = new UdpServer(port, ip);
us->Init();
us->run(func);
return 0;
}
如果udpclient客户端用这段代码逻辑实现,则会出现一个问题
while (1)
{
string message;
cout << "Enter#";
getline(cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&server, sizeof(server));
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, 0, 0);//不关心发送方的信息即可设为0
if (n > 0)
{
buffer[n] = 0;
cout << "server say:" << buffer << endl;
}
}
运行结果:
这个问题就是只有发消息的时候才能够接收到别人的消息,那么我们需要改进代码。用一个 线程读取消息,一个线程发送消息
更新udpclient.cc文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
//使用手册
void Usage(char* str)
{
printf("\n\t%s\tserverIp\tserverPort\n", str);
}
struct ThreadData
{
struct sockaddr_in _server;
int _sockfd;
};
void* sendMessage(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while (1)
{
string message;
cout << "Enter#";
getline(cin, message);
sendto(td->_sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&td->_server, sizeof(td->_server));
}
}
void* recvMessage(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
char buffer[1024];
while (1)
{
int n = recvfrom(td->_sockfd, buffer, sizeof(buffer), 0, 0, 0);//不关心发送方的信息即可设为0
if (n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
ThreadData td;
td._server = server;
td._sockfd = sockfd;
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, nullptr, sendMessage, &td);
pthread_create(&tid2, nullptr, recvMessage, &td);
pthread_join(tid1, 0);
pthread_join(tid2, 0);
return 0;
}
这样写貌似没有问题,但因为向屏幕打印信息也是共享资源,并没有对共享资源进行保护,所以会存在打印混乱的问题。
Enter#这个输入信息和收到聊天信息混到了一起。如果解决呢?
我们不用加锁,换种思维,因为linux一切皆文件,我们打开两个终端终端文件,一个终端专门去 标准错误 聊天信息,一个终端专门去 标准输出(已重定向到这一个终端) 需要输入的信息
用echo “hello” > /dev/pts/数字----查看自己的每个打开终端号是多少,然后你想显示聊天信息的终端是谁就将谁写到代码里面去
更新udpclient.cc文件
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Log.hpp"
//使用手册
void Usage(char* str)
{
printf("\n\t%s\tserverIp\tserverPort\n", str);
}
string terminal = "/dev/pts/1";//终端号为1。显示聊天信息的终端
int OpenTerminal()
{
int fd = open(terminal.c_str(), O_WRONLY);
if (fd < 0)
{
cerr << "open terminal error" << endl;
return 1;
}
dup2(fd, 2);
return 0;
}
struct ThreadData
{
struct sockaddr_in _server;
int _sockfd;
};
void* sendMessage(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while (1)
{
string message;
cout << "Enter#";
getline(cin, message);
sendto(td->_sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&td->_server, sizeof(td->_server));
}
}
void* recvMessage(void* args)
{
OpenTerminal();
ThreadData* td = static_cast<ThreadData*>(args);
char buffer[1024];
while (1)
{
int n = recvfrom(td->_sockfd, buffer, sizeof(buffer), 0, 0, 0);//不关心发送方的信息即可设为0
if (n > 0)
{
buffer[n] = 0;
cerr << buffer << endl;//更改为向标准错误打印
}
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
ThreadData td;
td._server = server;
td._sockfd = sockfd;
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, nullptr, sendMessage, &td);
pthread_create(&tid2, nullptr, recvMessage, &td);
pthread_join(tid1, 0);
pthread_join(tid2, 0);
return 0;
}
运行结果如下
实现简单的TCP网络程序
listen函数:第一个参数,输入型参数,socket函数创建的socket文件描述符。第二个参数,backlog后面章节会将,现在不需要了解,填5即可
返回值:成功返回0,错误返回-1
listen函数作用是什么呢?我们之前讲了tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种在等待连接到来的状态。listen就是让该参数socket的作用变为专门监听状态,监听是否有其它连接到来。如果有连接到来了该连接就会被捕获,在队列中(队列的大小就是backlog)等待被accept
第一个参数:处于监听状态的套接字。第二个参数,发送消息方的信息。第三个参数为第二个参数的大小
返回值:返回连接的套接字,能够直接通信的套接字。错误返回-1
udp只有一个文件描述符套接字,为什么tcp的accept参数有一个文件描述符套接字,返回还有一个socket,参数的socket和返回值的socket有什么区别?给大家举个例子
有一家农家乐餐馆,名字叫张三(listensocket,参数的socket),在门口专门负责拉客,有一个客人(客户端的信息)来了,张三就安排一个了服务员(accept返回的socket)为这位客人进行服务。
TCP因为是面向字节流的,所以可以直接用read和write来进行读写.对网络进行操作就是对文件进行操作
TcpServer.hpp文件
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include "Log.hpp"
using namespace std;
Log lg;
#define defaultIp "0.0.0.0"
#define defaultPort 8080
class TcpServer
{
public:
TcpServer(uint16_t port = defaultPort, const string& ip = defaultIp):_port(port), _ip(ip)
{}
void init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
lg(Fatal, "socket failed:%s", strerror(errno));
exit(1);
}
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(defaultIp);
server.sin_family = AF_INET; //等价于 把IP设为"0.0.0.0"
server.sin_port = htons(_port);
int n = bind(_listensock, (struct sockaddr*)&server, sizeof(server));
if ( n < 0)
{
lg(Fatal, "bind socket failed:%s", strerror(errno));
exit(2);
}
n = listen(_listensock, 5);
if (n < 0)
{
lg(Fatal, "listen socket failed:%s", strerror(errno));
exit(3);
}
}
void Service(int socket, const string& clientIp, uint16_t clientPort)
{
while (1)
{
char readBuffer[4096];
int n = read(socket, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
cout << readBuffer << endl;
string retstring = "tcpserver echo#";
retstring += readBuffer;
write(socket, retstring.c_str(), retstring.size());
}
else if (n == 0)//说明写端(客户端)关闭
{
lg(Info, "[clientIp:%s, clientPort:%d]:quit", clientIp.c_str(), clientPort);
break;
}
else
{
lg(Warning, "read error, sockfd:%d, clientIp:%s, clientPort:%d", socket, clientIp.c_str(), clientPort);
break;
}
}
}
void run()
{
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int socket = accept(_listensock, (struct sockaddr*)&client, &len);
string clinetIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
Service(socket, clinetIp, clientPort);
close(socket);
}
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
TcpServer.cc文件
#include "TcpServer.hpp"
#include <memory>
void Usage(char* proc)//使用手册
{
cout << "\n\t" << proc << "\t" << "port" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> ts(new TcpServer(port));//智能指针,不明白智能指针可看往期文章
ts->init();
ts->run();
return 0;
}
没写客户端,教大家一个工具telnet工具,可以当作tcp客户端进行使用.
运行结果
如果服务断开后立马再启动服务器,则可能产生错误
原因是什么,怎么不出现这种情况?后面章节会讲.
我们只需换个端口号即可
编写TCP客户端,客户端需不需要bind端口号?需要,但不需要自己显式bind端口号,操作系统会自动帮你绑定,那什么时候绑定呢?在第一次connect的时候会自动绑定
第一个参数为socket函数创建的套接字,第二个参数为服务器的信息,第三个参数为第二个参数的大小
TcpServer.cc文件
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
Log lg;
using namespace std;
void Usage(char* proc)//使用手册
{
cout << "\n\t" << proc << "\tserverIp\t" << "port" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
uint16_t serverPort = atoi(argv[2]);
string serverIp = argv[1];
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
server.sin_family = AF_INET;
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd < 0)
{
lg(Fatal, "socket failed:%s", strerror(errno));
exit(3);
}
int n = connect(socketfd, (const sockaddr*)&server, sizeof(server));
if (n < 0)
{
lg(Fatal, "connect failed:%s", strerror(errno));
exit(2);
}
while (1)
{
string s;
cout << "Enter#";
getline(cin, s);
write(socketfd, s.c_str(), s.size());
char readBuffer[4096];
int n = read(socketfd, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
cout << readBuffer << endl;
}
else if(n == 0)
{
cout << "server closed, me too" << endl;
exit(0);
}
else
{
perror("read failed");
exit(1);
}
}
return 0;
}
运行结果
我们服务器是单进程版本会有一个问题,只能服务一个进程,只有等这个进程结束才能够服务下一个进程.
下面我们来实现多进程版本的
更新TcpServer.hpp文件
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include "ThreadPool.hpp"
using namespace std;
#define defaultIp "0.0.0.0"
#define defaultPort 8080
class TcpServer;
struct ThreadData
{
ThreadData(TcpServer* ts, int socket, const string& clientIp, uint16_t clientPort)
:_this(ts), _socket(socket), _clientIp(clientIp), _clientPort(clientPort)
{}
TcpServer* _this;
int _socket;
const string& _clientIp;
uint16_t _clientPort;
};
class TcpServer
{
public:
TcpServer(uint16_t port = defaultPort, const string& ip = defaultIp):_port(port), _ip(ip)
{}
void init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
lg(Fatal, "socket failed:%s", strerror(errno));
exit(1);
}
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(defaultIp);
server.sin_family = AF_INET; //等价于 把IP设为"0.0.0.0"
server.sin_port = htons(_port);
int n = bind(_listensock, (struct sockaddr*)&server, sizeof(server));
if ( n < 0)
{
lg(Fatal, "bind socket failed:%s", strerror(errno));
exit(2);
}
n = listen(_listensock, 5);
if (n < 0)
{
lg(Fatal, "listen socket failed:%s", strerror(errno));
exit(3);
}
}
void Service(int socket, const string& clientIp, uint16_t clientPort)
{
while (1)
{
char readBuffer[4096];
int n = read(socket, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
cout << readBuffer << endl;//按行刷新,不写endl会卡在缓冲区
string retstring = "tcpserver echo#";
retstring += readBuffer;
write(socket, retstring.c_str(), retstring.size());
}
else if (n == 0)//说明写端(客户端)关闭
{
lg(Info, "[clientIp:%s, clientPort:%d]:quit", clientIp.c_str(), clientPort);
break;
}
else
{
lg(Warning, "read error:%s, sockfd:%d, clientIp:%s, clientPort:%d",strerror(errno), socket, clientIp.c_str(), clientPort);
break;
}
}
}
static void handler(int signo)
{
cout << "recv a signo:" << signo << endl;
while (waitpid(-1, nullptr, WNOHANG) > 0)
{
cout << "waitpid a process success" << endl;
}
}
static void* routine(void* args)
{
pthread_detach(pthread_self());//防止线程内存泄漏
ThreadData* td = static_cast<ThreadData*>(args);
td->_this->Service(td->_socket, td->_clientIp, td->_clientPort);
close(td->_socket);
}
void run()
{
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int socket = accept(_listensock, (struct sockaddr*)&client, &len);
string clinetIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
///
//单进程版本
//Service(socket, clinetIp, clientPort);
//close(socket);
///
//多进程版本2.1--有问题:和单进程版本没区别,该阻塞还是阻塞
// int n = fork();
// if (n == 0)
// {
// Service(socket, clinetIp, clientPort);
// }
// close(socket);
// int m = waitpid(n, 0, 0);
// if (m < 0)
// cout << "waitpid failed:" << strerror(errno) << endl;
///
//多进程版本2.2--有问题,如果多个子进程同时退出会内存泄漏
int n = fork();
if (n == 0)
{
Service(socket, clinetIp, clientPort);
}
waitpid(-1, 0, WNOHANG);
close(socket);
///
//多进程版本2.3--没有问题
// signal(SIGCHLD, handler);
// int n = fork();
// if (n == 0)
// {
// Service(socket, clinetIp, clientPort);
// exit(0);
// }
// close(socket);
///
//多进程版本2.4--没有问题
// int n = fork();
// if (n == 0)
// {
// if (fork() > 0)//孙子进程去执行代码,这样就能保证不会内存泄漏
// exit(1);
// Service(socket, clinetIp, clientPort);
// exit(0);
// }
// int m = waitpid(n, 0, 0);
// if (m < 0)
// cout << "waitpid failed:" << strerror(errno) << endl;
///
//多线程版本3
// pthread_t tid;
// ThreadData td(this, socket, clinetIp, clientPort);
// pthread_create(&tid, nullptr, routine, &td);
///
}
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
多进程2.3运行结果
多进程2.4运行结果
但是呢,创建多进程的代价太大了,向我们这种配置,最多连接20-30个客户端
所以就有多线程版本的,为了节省篇幅,我将多线程的代码也写到上面的代码中运行结果
多线程版本,有一个客户端到来就会创建一个线程,到来的时候才创建线程有点影响服务器效率了。并且客户不退出进程,服务器的线程也不退出,这是一种长服务,是不合理的,线程的数量是有限的,如果多进程是微型项目,这种版本只能适用于小型项目,如何解决?我们将服务器设置成为线程池版本就能解决,并取消服务器的循环服务并且服务完关闭该socket就能解决长服务问题。
因为需要投放给线程池任务,我们把客户端的访问变成任务。线程池不懂的可以看往期文章
TcpServer.hpp
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ThreadPool.hpp"
using namespace std;
#define defaultIp "0.0.0.0"
#define defaultPort 8080
class TcpServer
{
public:
TcpServer(uint16_t port = defaultPort, const string& ip = defaultIp):_port(port), _ip(ip)
{}
void init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
lg(Fatal, "socket failed:%s", strerror(errno));
exit(1);
}
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(defaultIp);
server.sin_family = AF_INET; //等价于 把IP设为"0.0.0.0"
server.sin_port = htons(_port);
int n = bind(_listensock, (struct sockaddr*)&server, sizeof(server));
if ( n < 0)
{
lg(Fatal, "bind socket failed:%s", strerror(errno));
exit(2);
}
n = listen(_listensock, 5);
if (n < 0)
{
lg(Fatal, "listen socket failed:%s", strerror(errno));
exit(3);
}
}
void run()
{
ThreadPool<Task>::GetInstance()->start();
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int socket = accept(_listensock, (struct sockaddr*)&client, &len);
string clinetIp = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
//线程池版本4
Task t(socket, clinetIp, clientPort);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
Task.hpp文件
#include "Log.hpp"
Log lg;
//构建线程池要执行的任务
class Task
{
public:
Task(int socket, const string& clientIp, uint16_t clientPort)
:_socket(socket), _clientIp(clientIp), _clientPort(clientPort)
{}
void run()
{
char readBuffer[4096];
int n = read(_socket, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
cout << readBuffer << endl;//按行刷新,不写endl会卡在缓冲区
string retstring = "tcpserver echo#";
retstring += readBuffer;
write(_socket, retstring.c_str(), retstring.size());
}
else if (n == 0)//说明写端(客户端)关闭
{
lg(Info, "[clientIp:%s, clientPort:%d]:quit", _clientIp.c_str(), _clientPort);
}
else
{
lg(Warning, "read error:%s, sockfd:%d, clientIp:%s, clientPort:%d",strerror(errno), socket, _clientIp.c_str(), _clientPort);
}
close(_socket);
}
void operator()()
{
run();
}
private:
int _socket;
const string& _clientIp;
uint16_t _clientPort;
};
ThreadPool.hpp文件
#include <vector>
#include <string>
#include <pthread.h>
#include <queue>
#include "Task.hpp"
using namespace std;
template<class T>
class ThreadPool;
template<class T>
struct pthreadInfo
{
pthreadInfo(pthread_t tid = 0, string threadname = "")
:_tid(tid), _threadname(threadname)
{}
pthread_t _tid;
string _threadname;
};
static const int defaultNum = 5;//线程池默认的个数
template<class T>
class ThreadPool
{
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
void ThreadSleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeUp()
{
pthread_cond_signal(&_cond);
}
string GetThreadName(pthread_t tid)
{
for (auto& thread : _threads)
{
if (thread._tid == tid)
{
return thread._threadname;
}
}
return nullptr;
}
public:
// 如果不加static该函数的参数列表中就会有隐藏的this指针,如果加了static不能访问类内成员了(因为静态成员不能访问非静态成员)
static void* HandlerTask(void* args)
{
ThreadPool<T>* tp = (ThreadPool<T>*)args;
while (1)
{
tp->Lock();
T task = tp->Pop();
tp->Unlock();
task();
}
}
void start()
{
for (int i = 0; i < _threads.size(); ++i)
{
_threads[i]._threadname = "threadname-";
_threads[i]._threadname += to_string(i + 1);
pthread_create(&_threads[i]._tid, nullptr, HandlerTask, this);
}
}
void Push(const T& in)
{
Lock();
_tasks.push(in);
ThreadWakeUp();
Unlock();
}
T Pop()
{
while (_tasks.empty())
{
ThreadSleep();
}
T task = _tasks.front();
_tasks.pop();
return task;
}
static ThreadPool<T> *GetInstance(int num = defaultNum)
{
if (nullptr == _tp)
{
pthread_mutex_lock(&_lock);
if (nullptr == _tp)
{
_tp = new ThreadPool<T>(num);
}
pthread_mutex_unlock(&_lock);
}
return _tp;
}
private:
//因为为单例模式:所以在类外不能使用构造和析构函数
//也不能使用拷贝构造和赋值重载函数
ThreadPool(int num = defaultNum)
:_threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool operator=(const ThreadPool<T>& ) = delete;
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
vector<pthreadInfo<T>> _threads; //所有的线程存储在vector里面
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;
TcpServer.cc文件
#include "TcpServer.hpp"
#include <memory>
void Usage(char* proc)//使用手册
{
cout << "\n\t" << proc << "\t" << "port" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> ts(new TcpServer(port));
ts->init();
ts->run();
return 0;
}
TcpClient.cc文件
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
Log lg;
using namespace std;
void Usage(char* proc)//使用手册
{
cout << "\n\t" << proc << "\tserverIp\t" << "port" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
uint16_t serverPort = atoi(argv[2]);
string serverIp = argv[1];
struct sockaddr_in server;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
server.sin_family = AF_INET;
while (1)
{
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd < 0)
{
lg(Fatal, "socket failed:%s", strerror(errno));
exit(3);
}
int n = connect(socketfd, (const sockaddr*)&server, sizeof(server));
if (n < 0)
{
lg(Fatal, "connect failed:%s", strerror(errno));
exit(2);
}
char readBuffer[4096];
string s;
cout << "Enter#";
getline(cin, s);
write(socketfd, s.c_str(), s.size());
n = read(socketfd, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
cout << readBuffer << endl;
}
else if(n == 0)
{
cout << "server closed, me too" << endl;
exit(0);
}
else
{
perror("read failed");
exit(1);
}
}
return 0;
}
运行结果
TCP实现一个中英互译程序
因为我们将任务和代码进行了解耦,所以我们只需要设计重新设计Task即可完成这个程序
dict.txt文件
banana:香蕉
apple:苹果
green:绿色
red:红色
Init.hpp文件
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
const string dictname = "./dict.txt";
const string sep = ":";
Log lg;
//yellow:黄色
bool Split(const 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);
}
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;
};
Task.hpp文件
#include "Init.hpp"
Init init;
//构建线程池要执行的任务
class Task
{
public:
Task(int socket, const string& clientIp, uint16_t clientPort)
:_socket(socket), _clientIp(clientIp), _clientPort(clientPort)
{}
void run()
{
char readBuffer[4096];
int n = read(_socket, readBuffer, sizeof(readBuffer) - 1);
if (n > 0)
{
readBuffer[n] = 0;
string retstring = init.translation(readBuffer);
write(_socket, retstring.c_str(), retstring.size());
}
else if (n == 0)//说明写端(客户端)关闭
{
lg(Info, "[clientIp:%s, clientPort:%d]:quit", _clientIp.c_str(), _clientPort);
}
else
{
lg(Warning, "read error:%s, sockfd:%d, clientIp:%s, clientPort:%d",strerror(errno), socket, _clientIp.c_str(), _clientPort);
}
close(_socket);
}
void operator()()
{
run();
}
private:
int _socket;
const string& _clientIp;
uint16_t _clientPort;
};
运行结果
守护进程原理
一个用户登陆的时候会产生一个session,session中有前台进程和后台进程,前台进程只能有一个(默认为bash,bash就是我们平常敲的指令会发送给它,它会帮我们解析命令并执行对应的操作)后台进程可以有多个。往期文章中也有讲解
jobs命令能查看一个会话中的作业(任务)
输入&能使该进程在后台运行,[数字]的数字表示作业号
fg [作业号]能将该进程放到前台执行
bg [作业号]能将该进程放到后台执行
进程组id和作业任务有什么关系呢?
作业任务是一件具体的事情,可以由一个人完成,也可以由多个人完成。一个作业任务是由一组进程完成的。会话session是由作业任务来划分的
当PID和PGID相等时,则这个进程是组长,PID和SID的时候它是bash进程
当用户登陆时,这个session会自动创建,当用户退出时,这个session会自动销毁,里面的进程也会跟着销毁,如何不受到用户登陆和退出的影响呢?
让该进程自成一个会话。和终端设备无关的的进程叫做守护进程
谁调用这个函数,谁就自建一个会话,前提时该进程不能是进程组的组长。原因很简单,自己是包工头,包工头跑了你手下的人怎么办。
问题:如何保证不是组长进程呢?可以让自己的子进程来执行
实现一个Daemon进程
1.让调用进程忽略掉异常信号,防止对关闭的文件描述符操作而使进程收到异常的信号退出
2.让自己不是组长
3.守护进程使脱离终端的,关闭或者重定向一切进程默认打开的文件
(/dev/null 这个文件就如同一个黑洞,文件一直处于空的状态,但存在。所以可以以温和的方式重定向0、1、2,而不是直接关闭,如果关闭了使用printf函数可能导致进程崩溃)
4.更改工作目录(也可以不更改)
Daemon.hpp文件
#include <iostream>
#include <string>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
const string nullfile = "/dev/null";
void Daemon(const string& cwd = "")
{
//1.忽略其它异常信号
signal(SIGCHLD, 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);
}
}
test.cc文件
#include <iostream>
#include <unistd.h>
#include "Daemon.hpp"
using namespace std;
int main()
{
Daemon();
while (1)
{
sleep(1);
cout << "hello world" << endl;
}
return 0;
}
运行结果
可以看到,全部都退了后重新登陆,该进程仍然没退出
不想自己写,有没有更简单的呢?当然系统也给我们提供了这样的接口
第一个参数:是否更改进程的工作目录,如果填0,则更改到根目录下,否则仍然在当前目录下
第二个参数:是否将0\1\2重定向到/dev/null.如果填0,则重定向,否则不更改