网络编程
1. 套接字预备知识
1.1 理解源IP地址和目的IP地址
IP地址(公网IP)是用来唯一标识互联网中的一台主机。
IP地址的作用:可以在网络当中唯一标识一台主机,一个公网IP地址只能被一台机器所占有,一个机器可以拥有多个IP地址。
- 源IP地址:表示该条信息来源于哪个机器。
- 目的IP地址:表示该条信息去往于哪个进程。
1.2 认识端口号
数据从计算机A到达计算机B,并不是真正的目的,而是到达计算机B的某一个进程,提供数据处理的服务,才是网络传输数据最终的目的。
数据本身并不是由计算机产生的,而是由人,即用户通过特定的客户端等等输入进去的,因此本质上,所有的网络通信,站在人的角度上,就是人与人之间的通信。站在计算机角度上,是进程间通信!只不过通信的进程不在一台计算机上。就比如抖音的app客户端,它是一个进程,抖音的服务器,也是一个进程。我们通过抖音客户端达到网络通信,在抖音的服务器上获取信息,所以网络本质上就是进程间的通信。
而IP地址,仅仅是解决了两台物理机器之间的相互通信的识别问题,我们还要解决是在这两台计算机之间的进程间的通信,就是怎么知道计算机A发出的信息是要传给计算机B中的某个进程呢?这就需要端口号了!
因此,端口号的作用是唯一标识一台机器上的唯一 一个进程!通过IP+端口号port,就能够标识互联网中的唯一 一个进程!
我们可以将整个网络看成是一个大的OS,所有的网络行为,几乎都是在这一个大的OS进行进程间通信!
既然说端口号port是进程的一个身份,那么进程的PID按理论上来说,也能通过PID来进行网络上的进程间通信,那么为什么还需要一个port呢?
(1)IP与端口号和PID/端口号的区别:
IP的标识物理机器的,port是标识进程的。而PID也是用来标识进程的,也是唯一性的!其实PID跟port,都属于进程的身份,就好像学生由身份证,也有他的学生证,所以将进程的PID和port分开来使用,是为了解耦!
一个进程可以关联多个端口号,而一个端口号不能关联多个进程。
(2)网络是一份共享资源
要在网络上进行进程间通信,我们首先需要找到目标主机,然后找到该主机上的服务(进程),完成进程间通信。而进程要通信的话,由于进程具有独立性,因此不同的进程必须看到同一份资源,即共享资源!所有,网络便是一份共享资源!
(3)源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,本质上就是在描述 “数据是谁发的,要发给谁”。
1.3 认识TCP/UDP协议
先简单得对TCP和UDP来一个直观的认识:
TCP协议和UDP协议都是传输层的控制协议,以下是两种协议的特点,我们需要根据它们的特定,在不同场景下,权衡使用哪种协议。
TCP协议:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
注意:这里的可靠传输和不可靠传输并不是贬义词,而是中性的,不要看到TCP可靠就无脑用,可靠也是有成本的。像一些重要的消息发送如银行等就必须使用TCP协议,像平常聊天,直播都是可以用UDP协议来实现的。
1.4 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
网络数据流觉得这样分来分去太麻烦了,所以网络数据就规定自己使用大端的形式。如果你的数据流本来就是大端,那你就直接传输,如果你的数据流是小端,就要先转换成大端,再来传输!
因此,网络字节序指的就是在网络上的采用的大端形式,先发出的数据是低地址,后发出的数据是高地址。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostshort); //返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netlong); //返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netshort); //返回值:以主机字节序表示的16位整数
- 快速记忆函数:h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
总结:
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。
1.5 地址转换函数
有时候,我们需要打印出被人能理解的地址格式(如:12.0.0.1)而不是被计算机理解的地址格式(32位二进制数),那么就需要用到以下函数。
它们在ASCII字符串(这是人们偏爱使用的格式)与网络字节序的二进制值(这是存放在套接字地址结构中的值)之间转换网际地址。
#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
char* inet_ntoa(struct in_addr addrptr);
- inet_aton函数
该函数将strptr所指C字符串转换成一个32位的网络字节序二进制值,并且通过指针addrptr来存储。若成功则返回1,否则返回0;
如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效检查,但不存储任何结果。
- inet_ntoa函数
该函数讲一个32位的网络字节序二进制IPV4地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。
- inet_addr函数
与inet_aton一样进行相同的转换,返回值为32的网络字节序二进制值。
2. UDP套接字接口
套接字就是socket,用于描述IP地址和端口号,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。
// 创建 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);
// 用于接收数据 (UDP, 服务器)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
// 用于发送数据 (UDP, 服务器)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
2.1 socket函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)。
参数:
- domain:协议域。就是需要用哪种协议,我们最常用的就两种,分别是AF_INET (IPV4协议)和AF_INET6 (IPV6协议)。
- type:套接字的类型,即SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。
- protocol:这个我们一般设置为0即可,它是用来制定某个协议的特定类型,即type类型中的某个类型。通常一种协议只有一种类型,那样该参数可以直接被设置为0;如果协议有多种类型,则需要指定协议类型。
返回值:返回一个文件描述符。也即是套接字描述符。
2.2 bind函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
功能:绑定端口号 (TCP/UDP, 服务器)。
参数:
- socket:socket函数返回的文件描述符。
- address:指定想要绑定的IP和端口。后面解析什么是sockadder结构体。
- address_len:address的长度。
返回值:成功为0,失败-1。
2.3 recvfrom函数
函数原型:
//头文件
#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);
功能:用于接收数据 (UDP, 服务器)。
参数:
- sockfd:套接字文件描述符。
- buf:指明一个缓冲区,该缓冲区用来存放recvfrom函数接收到的数据。
- len:buf的长度。
- flags:一般设置为0,表示阻塞状态。
- src_addr:是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。
- addrlen:src_addr的大小。
返回值:成功返回接收到的字节数。失败返回-1。
2.4 sendto函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能:用于发送数据 (UDP, 服务器)。
参数:
- sockfd:套接字文件描述符。
- buf:指明一个存放应用程序要发送数据的缓冲区。
- len:buf的长度。
- flags:一般设置为0,表示阻塞状态。
- dest_addr:表示目地机的IP地址和端口号信息
- addrlen:dest_addr的大小。
返回值:成功返回发送的字节数。失败返回-1。
2.5 sockaddr结构体
网络通信的方式有很多种,比如基于网IP的网络通信,AF_INET,原始套接字,域间套接字等等。有那么多方式,那么在绑定IP和端口的时候,就需要很多种方法了,因此系统需要将其统一 一下结构,就单独设计了一个通用的套接字地址结构(sockaddr_in),我们只要给这个通用的套接字地址结构传入不同的套接字地址结构,然后进行强转。在地址结构中给到我们想要通信的IP地址、端口号以及所采用的协议族。
- 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结构体指针做为参数;
(1)sockaddr结构:
struct sockaddr
{
__SOCKADDR_COMMON (sa_);
char sa_data[14];
};
(2)sockaddr_in结构:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
/* 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地址。
(3)简化看看表示IPV4的结构体:
struct sockaddr_in
{
sa_family_t sin_family;//地址族
uint16_t sin_port;//TCP/UDP端口号,16位整型
struct in_addr sin_addr;//IP地址,32位整型
char sin_sero[8];//不需要管
};
(4)其中sin_family:
地址族 | 含义 |
---|---|
AF_INET | IPV4网络协议中的使用的地址族 |
AF_INET6 | IPV6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
(5)in_addr结构:
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数。
现在可以写一个简单的UDP协议的服务器了。
3. 简单的UDP网络程序
(1)实现一个UDP网络通信功能,在client中输入信息,会在server中显示出来,并且返回信息给client,达到网络通信聊天的效果。
①客户端client代码:
#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>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[2];
uint16_t serverport = std::stoi(argv[1]);
sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while(1)
{
std::cout << "Please Enter@ ";
getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
/*if(s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}*/
}
close(sockfd);
return 0;
}
②服务器server代码:
- 在编写服务器代码时我们要注意云服务器是禁止绑定(bind)公网IP的,但是我们写服务器时一般都不绑定特定的IP,因为有可能一台服务器有多个网卡,如果只绑定一个,那其它IP就收不到消息了。
- 端口号在1024之前的一般都不会允许你绑定,这是因为1024之前都是系统内定的端口号,一般都是有固定的应用层协议使用。
#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 <memory>
#include <cstdio>
#include "Log.hpp"
Log lg;
enum {
SOCKET_ERR=1,
BIND_ERR
};
typedef std::function<std::string(const std::string&)> func_t;
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const std::string& ip = defaultip) //构造函数
:_port(port)
,_ip(ip)
,_isrunning(false)
,_sockfd(0)
{
}
void Init()
{
// 1. 创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
lg(Fatal, "socket create error, sockfd: %d", _sockfd);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", _sockfd);
// 2. bind socket
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());
int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));
if(n < 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(func_t func)
{
_isrunning = true;
char inbuffer[size];
while(_isrunning)
{
sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
std::string info = inbuffer;
//std::string echo_string = "Server get a message: " + info;
//std::cout << echo_string << std::endl;
std::string echo_string = func(info);
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);
}
}
~UdpServer() //析构函数
{
if(_sockfd > 0)
{
close(_sockfd);
}
}
private:
std::string _ip; // 任意地址bind 0
int _sockfd; // 网路文件描述符
uint16_t _port; // 表明服务器进程的端口号
bool _isrunning;
};
#include "UdpServer.hpp"
std::string hander(const std::string& str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
int main()
{
std::unique_ptr<UdpServer> svr(new UdpServer());
svr->Init();
svr->run(hander);
return 0;
}
③日志类:
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#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 LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// // va_list s;
// // va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// // va_end(s);
// // 格式:默认部分+自定义部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暂时打印
// printLog(level, logtxt);
// }
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
④运行结果:
(2)实现Windows系统和Linux系统下的通信,Windows充当客户端发送消息,Linux为服务端处理消息并返回给客户端。
①Windows客户端代码:
#include <iostream>
#include <string>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
uint16_t serverport = 8080;
const std::string serverip = "115.159.193.163";
int main()
{
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
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());
int len = sizeof(server);
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while (1)
{
std::cout << "Please Enter@ ";
getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
/*if(s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}*/
}
closesocket(sockfd);
WSACleanup();
return 0;
}
Windows下的网络通信和Linux下的有些许不同,但是接口基本上是一致的。
服务器代码和第一个程序一样。
②运行结果:
(3)UDP实现群聊信息框,当其它用户创建时服务端显示用户创建成功。
①客户端client代码:
注意:客户端代码必须要使用多线程来实现,要不然发送消息和就会阻塞,无法收到其他用户发送过来的消息。
#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>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
//std::string serverip;
~ThreadData()
{
close(sockfd);
}
};
void* recv_message(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
std::string message;
socklen_t len = sizeof(td->server);
while(1)
{
std::cout << "Please Enter@ ";
getline(std::cin, message);
sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);
}
}
void* send_message(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
char buffer[1024];
while(1)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[2];
uint16_t serverport = std::stoi(argv[1]);
ThreadData td;
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport);
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(td.sockfd < 0)
{
std::cout << "socker error" << std::endl;
return 1;
}
pthread_t recvr;
pthread_t sender;
pthread_create(&sender, nullptr, send_message, &td);
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_join(recvr,nullptr);
pthread_join(sender,nullptr);
return 0;
}
②服务器server代码:
#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 <memory>
#include <unordered_map>
#include <cstdio>
#include "Log.hpp"
Log lg;
enum {
SOCKET_ERR=1,
BIND_ERR
};
typedef std::function<std::string(const std::string&)> func_t;
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const std::string& ip = defaultip) //构造函数
:_port(port)
,_ip(ip)
,_isrunning(false)
,_sockfd(0)
{
}
void Init()
{
// 1. 创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
lg(Fatal, "socket create error, sockfd: %d", _sockfd);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", _sockfd);
// 2. bind socket
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());
int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));
if(n < 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 CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport)
{
auto iter = _online_user.find(clientip);
if(iter == _online_user.end())
{
_online_user.insert({
clientip, client});
std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
}
}
void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
{
for(const auto &user : _online_user)
{
std::string message = "[";
message += clientip;
message += ":";
message += std::to_string(clientport);
message += "]# ";
message += info;
socklen_t len = sizeof(user.second);
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
}
}
void run()
{
_isrunning = true;
char inbuffer[size];
while(_isrunning)
{
sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
CheckUser(client, clientip, clientport);
std::string info = inbuffer;
Broadcast(info,clientip, clientport);
}
}
~UdpServer() //析构函数
{
if(_sockfd > 0)
{
close(_sockfd);
}
}
private:
std::string _ip; // 任意地址bind 0
int _sockfd; // 网路文件描述符
uint16_t _port; // 表明服务器进程的端口号
bool _isrunning;
std::unordered_map<std::string, struct sockaddr_in> _online_user;
};
int main()
{
std::unique_ptr<UdpServer> svr(new UdpServer());
svr->Init();
svr->run();
return 0;
}
③运行结果:
4. 关于地址转换函数
以下都是基于IPv4的socket网络编程。sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址。
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换:
(1)字符串转in_addr的函数:
(2)in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* addrptr。
(3)关于inet_ntoa
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。
那么是否需要调用者手动释放呢?
man手册上说明inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。
那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
- 思考:如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?答:会出现异常
- 在APUE中,明确提出inet_ntoa不是线程安全的函数;
- 但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁;
- 在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
5. TCP套接字接口
socket函数和bind函数TCP也是需要的,函数原型见本章第二小节。
// 开始监听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);
5.1 listen函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
int listen(int socket, int backlog);
功能:开始监听socket (TCP, 服务器)。
参数:
- socket:socket函数返回的文件描述符。
- backlog:指定内核为相应套接字排队的最大连接个数(通常设为5~10)。
返回值:成功为0,失败-1。
5.2 accept函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
功能:接收请求 (TCP, 服务器)。
参数:
- socket:socket函数返回的文件描述符。
- address:指定想要绑定的IP和端口。
- address_len:address的长度。
返回值:内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。我们常常称它的第一个参数为监听套接字(listening socket) 描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字(connected socket) 描述符。区分这两个套接字非常重要。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。
总的来说,函数accept所返回的文件描述符是新的套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持监听状态并接受其他连接请求。
5.3 connect函数
函数原型:
//头文件
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:建立连接 (TCP, 客户端)。
参数:
- sockfd:socket函数返回的文件描述符。
- addr:指定想要绑定的IP和端口。
- addrlen:address的长度。
返回值:成功为0,失败-1。
6. 简单的TCP网络程序
实现一个TCP网络通信功能,在client中输入信息,在server处理好消息后,将处理好的信息返回给client。
(1)客户端client代码:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[1]);
exit(1);
}
std::string clientip = argv[1];
uint16_t clientport = std::stoi(argv[2]);
sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(clientport);
inet_pton(AF_INET, clientip.c_str(), &(client.sin_addr));
while (1)
{
int cnt = 5;
int isreconnect = false;
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
do
{
// tcp客户端要bind,但是不要显示的bind。系统进行bind,随机端口
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&client, sizeof(client));
if (n < 0)
{
isreconnect = true;
cnt--;
std::cerr << "connect error..., reconnect: " << cnt << std::endl;
sleep(1);
}
else
{
break;
}
}while(isreconnect && cnt);
if(cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
std::string message;
std::cout << "Please Enter# ";
std::getline(std::cin, message);
int m = write(sockfd, message.c_str(), message.size());
if (m < 0)
{
std::cerr << "write error..." << std::endl;
}
char inbuffer[4096];
ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
close(sockfd);
}
return 0;
}
(2)代码解析:
6.1 单进程版服务器
(1)server代码:
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "Log.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;
class TcpServer
{
public:
TcpServer(const uint16_t& port, const std::string& ip = defaultip)
:_port(port)
,_ip(ip)
,_listensock(defaultfd)
{
}
void Init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if(_listensock < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_aton(_ip.c_str(), &(local.sin_addr));
socklen_t len = sizeof(local);
int n = bind(_listensock, (const sockaddr*)&local, len);
if(n < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
int m = listen(_listensock, backlog);
if(m < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void run()
{
lg(Info, "tcpServer is running....");
while(1)
{
// 1.获取连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 单进程
Server(sockfd, clientport, clientip);
close(sockfd);
}
}
void Server(int sockfd, const uint16_t& clientport, const std::string clientip)
{
char buffer[4096];
while(1)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
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;
}
}
}
~TcpServer()
{
}
private:
int _listensock;
std::string _ip;
uint16_t _port;
};
int main()
{
std::unique_ptr<TcpServer> tcp_svr(new TcpServer(8080));
tcp_svr->Init();
tcp_svr->run();
return 0;
}
(2)代码解析:
6.2 多进程版服务器
(1)server代码:
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;
class TcpServer
{
public:
TcpServer(const uint16_t& port, const std::string& ip = defaultip)
:_port(port)
,_ip(ip)
,_listensock(defaultfd)
{
}
void Init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if(_listensock < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_aton(_ip.c_str(), &(local.sin_addr));
socklen_t len = sizeof(local);
int n = bind(_listensock, (const sockaddr*)&local, len);
if(n < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
int m = listen(_listensock, backlog);
if(m < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void run()
{
lg(Info, "tcpServer is running....");
while(1)
{
// 1.获取连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 多进程
pid_t id = fork();
if(id == 0)
{
close(_listensock); //关闭父进程继承过来的套接字,使父进程单独管理自己的套接字
if(fork() > 0) //子进程创建子进程,子进程退出,由孙子进程执行
{
exit(0);
}
//孙子进程, system 领养
Server(sockfd, clientport, clientip);
close(sockfd);
exit(0);
}
//father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
}
void Server(int sockfd, const uint16_t& clientport, const std::string clientip)
{
char buffer[4096];
while(1)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
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;
}
}
}
~TcpServer()
{
}
private:
int _listensock;
std::string _ip;
uint16_t _port;
};
(2)代码解析(套接字解析见上图):
6.3 多线程版服务器
(1)server代码:
#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 "Log.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;
class TcpServer;
struct ThreadData
{
ThreadData(int fd, const std::string& ip, const uint16_t& port, TcpServer* t)
:_ip(ip)
,_port(port)
,_sockfd(fd)
,_tsvr(t)
{
}
std::string _ip;
uint16_t _port;
int _sockfd;
TcpServer* _tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t& port, const std::string& ip = defaultip)
:_port(port)
,_ip(ip)
,_listensock(defaultfd)
{
}
void Init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if(_listensock < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_aton(_ip.c_str(), &(local.sin_addr));
socklen_t len = sizeof(local);
int n = bind(_listensock, (const sockaddr*)&local, len);
if(n < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
int m = listen(_listensock, backlog);
if(m < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void run()
{
lg(Info, "tcpServer is running....");
while(1)
{
// 1.获取连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 多线程
ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
}
static void* Routine(void* argv)
{
pthread_detach(pthread_self()); //线程分离,不需要主线程管它死活
ThreadData* td = static_cast<ThreadData*>(argv);
td->_tsvr->Server(td->_sockfd, td->_port, td->_ip);//???
delete td;
return nullptr;
}
void Server(int sockfd, const uint16_t& clientport, const std::string clientip)
{
char buffer[4096];
while(1)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
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;
}
}
}
~TcpServer()
{
}
private:
int _listensock;
std::string _ip;
uint16_t _port;
};
6.4 线程池版服务器
(1)server代码:
#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 "Log.hpp"
#include "Threadpool.hpp"
#include "Task.hpp"
#include "Init.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;
class TcpServer;
struct ThreadData
{
ThreadData(int fd, const std::string& ip, const uint16_t& port, TcpServer* t)
:_ip(ip)
,_port(port)
,_sockfd(fd)
,_tsvr(t)
{
}
std::string _ip;
uint16_t _port;
int _sockfd;
TcpServer* _tsvr;
};
class TcpServer
{
public:
TcpServer(const uint16_t& port, const std::string& ip = defaultip)
:_port(port)
,_ip(ip)
,_listensock(defaultfd)
{
}
void Init()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if(_listensock < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_aton(_ip.c_str(), &(local.sin_addr));
socklen_t len = sizeof(local);
int n = bind(_listensock, (const sockaddr*)&local, len);
if(n < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
int m = listen(_listensock, backlog);
if(m < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(1);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void run()
{
ThreadPool<Task>::GetInstance()->Start();
lg(Info, "tcpServer is running....");
while(1)
{
// 1.获取连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 线程池
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
static void* Routine(void* argv)
{
pthread_detach(pthread_self()); //线程分离,不需要主线程管它死活
ThreadData* td = static_cast<ThreadData*>(argv);
td->_tsvr->Server(td->_sockfd, td->_port, td->_ip);//???
delete td;
return nullptr;
}
void Server(int sockfd, const uint16_t& clientport, const std::string clientip)
{
char buffer[4096];
while(1)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
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;
}
}
}
~TcpServer()
{
}
private:
int _listensock;
std::string _ip;
uint16_t _port;
};
(2)Task.hpp:
#pragma once
#include <iostream>
#include <string>
#include "Init.hpp"
enum{
DivZero=1,
ModZero,
Unknown
};
Init init;
class Task
{
public:
Task(int sockfd, const std::string& clientip, const uint16_t& clientport)
:_sockfd(sockfd)
,_ip(clientip)
,_port(clientport)
{
}
void run()
{
char buffer[4096];
int 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()); // 100 fd 不存在
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", _ip.c_str(), _port, _sockfd);
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", _sockfd, _ip.c_str(), _port);
}
close(_sockfd);
}
void operator ()()
{
run();
}
~Task()
{
}
private:
int _sockfd;
std::string _ip;
uint16_t _port;
};
(3)Init.hpp:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
Log lg;
const std::string dictname = "./dict.txt";
const std::string sep = ":";
//yellow:黄色...
static bool Split(std::string& s, std::string* part1, std::string* part2)
{
auto pos = s.find(sep);
if(pos == std::string::npos)
{
return false;
}
*part1 = s.substr(0, pos);
*part2 = s.substr(pos + 1);
return true;
}
class Init
{
public:
Init()
{
std::ifstream in(dictname);
if(!in.is_open())
{
lg(Fatal, "ifstream open %s error", dictname.c_str());
exit(1);
}
std::string line;
while(std::getline(in, line))
{
std::string part1, part2;
Split(line, &part1, &part2);
dict.insert({
part1, part2});
}
in.close();
}
std::string translation(const std::string& key)
{
auto iter = dict.find(key);
if(iter == dict.end())
{
return "Unknow";
}
else
{
return iter->second;
}
}
private:
std::unordered_map<std::string, std::string> dict;
};
(4)dict.txt:
apple:苹果
banana:香蕉
red:红色
yellow:黄色
the: 这
be: 是
to: 朝向/给/对
and: 和
I: 我
in: 在...里
that: 那个
have: 有
will: 将
for: 为了
but: 但是
as: 像...一样
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他们
we: 我们
their: 他们的
his: 它的
with: 和...一起
she: 她
he: 他(宾格)
it: 它
(5)英文翻译中文的服务器运行结果:
(6)TCP协议整体流程图:
7. 守护进程
7.1 守护进程的概念
守护进程也叫做精灵进程,是运行在后台的一种特殊进程他独立于控制终端并且可以周期性的执行某种任务或者等待处理某些发生的事件。
守护进程是非常有用的进程,在Linux当中大多数服务器用的就是守护进程比如Web服务器httpd等,同时守护进程完成很多系统的任务。当Linux系统启动的时候,会启动很多系统服务,这些进程服务是没有终端的也就是说你把终端关闭了这些系统服务是不会停止的,他们一直运行着他们有一个名字就叫做守护进程。
一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。
7.2 进程组和会话
(1)进程组的概念:
- 进程除了有进程的PID之外还有一个进程组,进程组是由一个进程或者多个进程组成。通常他们与同一作业相关联可以收到同一终端的信号。
- 每个进程组有唯一的进程组ID,每个进程组有一个进程组组长。如何判断一个进程是不是这个进程组的组长了,通常进程组ID等于进程ID那么这个进程就是对应进程组的组长。
(2)会话的概念:
- 会话是有一个或者多个进程组组成的集合。
- 一个会话可以有一个终端,建立与控制终端连接的会话首进程被成为控制进程,一个会话的几个进程组可以分为前台进程和后台进程,而这些进程组的控制终端相同也就是sesion id是一样的当用户使用ctr +c 产生SIGINT信号时内核会发送信号给相应前台进程组的所有进程如果我运行一个程序我们想要把他放到后台运行我们可以在可执行程序的后面加一个&。例如:./test & 如果我们想要把他提到前台进程我们可以使用fg。
(3)使用如下监控脚本观察一个现象:
[xiaomaker@VM-28-13-centos code_cpp]$ ps axj | head -1 && ps axj | grep sshd
上述第一行以 -D 结尾的就是服务器(守护进程)。它的PPID是1。下面来介绍上述选项的意义:
- COMMAND:启动的进程命令名称
- TIME:进程启动的时长
- UID:是谁启动的
- STAT:状态
- TPGID:当前进程组和终端的关系(如果是-1,则没有任何关系)
- TTY:代表哪一个终端
- SID:当前进程的会话ID
- PGID:当前进程所属的进程组
- PID:当前进程自己的ID
- PPID:当前进程的父进程的ID
(4)我们将三个进程放到后台运行,并使用如下监控脚本观察现象:
[xiaomaker@VM-28-13-centos code_cpp]$ ps ajx | head -1 && ps axj | grep sleep
上述创建了三个进程,可以确定的是这三个进程的PPID都是一样的,因为父进程都是Bash。上述三个进程是属于同一个进程组(PGID)的,且会发现启动的第一个进程是进程组的组长.
上述三个进程的会话ID(SID)为2302,即bash。我们通过下面的监控脚本观察:
如下图:
一旦我们登陆linux,linux会我们创建一个会话。会话内部由多个进程组构成。登陆后会给我们加载bash,所以其内部必须有一个前台进程组(任何时刻,只能有一个前台进程组)。0个或多个后台进程组。
再使用如下监控脚本观察上述的会话2302:
[xiaomaker@VM-28-13-centos code_cpp]$ ps axj | head -1 && ps ajx | grep 6821
图中可以看出,bash自己就是一个进程,自己就是进程组的组长,也是会话中的老大(会话前台进程组)。 所以一开始创建的三个进程,它们的会话SID均为2302,即bash
后续如果我们再自己启动新进程或者是启动进程组,它依旧属于bash自己的会话。
我们可以使用jobs命令查看系统中的任务:
先前这三个进程是放到后台运行的。我们使用fg 1(fg后面跟的1是后台进程的序列号,每个后台进程都有自己的序列号)命令把此任务提到前台:
当把后台进程提到前台后,会发现我shell命令用不起来了。因为我们只能有一个前提进程组。当我们把刚才的后台进程提到前台,那么我bash命令行解释器会自动退到后台进程组 。那么自然就没有办法接受你的输入了。
综上可知:
- 我们在命令行中启动一个进程,现在就可以叫做在会话中启动一个进程组,来完成某种任务。
- 所有会话内的进程fork创建子进程,一般而言依旧属于当前会话。
像平时当我们觉得windows卡顿的时候,我们可能会重新注销一下。注销就是让用户退出登陆后再重新登陆,那么此时就相当于给你新建一个会话。卡顿是因为你本次登陆过程中启动了很多任务,且都属于同一个会话,注销本质就是把你内部会话的所有进程组删掉。
注意:
- 在登录的状态时,新起了一个网络服务器,创建好之后,再派生的子进程也属于当前会话,所以我们就不能让这个网络服务器属于这个会话内容,要不然它会受到用户的登录和注销的影响。
- 所以当我们有个网络服务的时候,应该脱离这个会话,让它独立的在计算机里自成进程组,自成新会话。这样在两个用户同时登录的时候,形成的两个会话是独立的,在操作各自的bash不会互相影响。
- 像这种自成进程组,自成新会话,而且周而复始进行的进程称为守护进程(精灵进程)。
8. TCP网络程序(守护进程化)
8.1 守护进程化的方式
有三种方式让自己的进程守护进程化:
- 自己写daemon函数,推荐使用这种方式(下面的TCP网络程序中的daemon函数就是自己模拟实现的)
- 用系统的daemon函数
- nohup命令
8.2 自定义daemon函数
本章第六节编写的TCP网络程序是在前台进行的,但是实际上服务器并不是在前台运行的,而是在后台进行的。所以现在对TCP网络程序的代码进行修改,使其守护进程化。让服务器在后台运行。我们创建daemon.hpp文件完成守护进程的主要逻辑。代码逻辑如下:
- 调用signal函数忽略SIGPIPE信号
- 更改进程的工作目录(可以不更改)
- fork子进程,exit退出父进程。让自己不要成为进程组组长。从而保证后续不会再和其他终端相关联。
- 调用setsid函数设置自己是一个独立的会话
- 将标准输入、标准输出、标准错误重定向到/dev/null。
守护进程需要调用setsid函数,需要注意:
- 调用setsid创建新会话的目的,是让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)。
- 调用setsid创建新会话时,要求调用进程不能是进程组组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程我们直接让其退出即可。此时子进程就不是组长进程了,而是独立会话的守护进程。
- 当服务端给客户端写入时,但是客户端突然关掉了,那就是向一个不存在的文件描述符写入,此时服务端会收到SIGPIPE信号而自动终止
- 当前进程有自己的工作目录,有时候守护进程想要更改自己的工作目录,一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。我们可以使用chdir更改进程的工作目录,不过此操作不强求。
- 守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出、标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),类似于Linux下的一个“文件黑洞” or “垃圾桶”,通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)
(1)dameon.hpp:
#pragma once
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void Dameon()
{
// 1、忽略SIGPIPE
signal(SIGPIPE, SIG_IGN);
// 2、更改进程的工作目录
// chdir();
// 3、让自己不要成为进程组组长
if (fork() > 0)
{
exit(0);
}
// 4、设置自己是一个独立的会话
setsid();
// 5、重定向0,1,2
int fd = open("/dev/null", O_RDWR);
if (fd > 0) // fd == 3
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
// 6、关闭掉不需要的fd
close(fd);
}
}
(2)我们只需要在服务端调用此Daemon函数即可:
(3)现在我们运行服务端,通过下面的监控脚本辅助观察信息:
[xiaomaker@VM-28-13-centos test_01_31]$ ps axj | head -1 && ps axj | grep test
[xiaomaker@VM-28-13-centos test_01_31]$ ps axj | head -1 && ps axj | grep sshd
运行代码,用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示的是问好,也就意味着该进程已经与终端去关联了。此外PID、PGID、SID均是一样的,可以看出已经是守护进程了:
现在就相当于把代码部署到了Linux中,现在运行客户端,能够正常与服务端通信。即使我们把电脑关掉了,此服务端也是一直在运行的。
(4)运行结果:
(5)我们要终结一个守护进程只需要kill -9杀掉此进程就可以。
8.3 调用系统的daemon函数
函数原型:
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数:
- 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
- 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理。
调用示例:
#include <unistd.h>
int main()
{
daemon(0, 0);
while (1);
return 0;
}
- 调用daemon函数创建的守护进程与我们原生创建的守护进程差距不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程。
- 也就是说系统实现的daemon函数没有防止守护进程打开终端,因此我们实现的反而比系统更加完善。
8.4 nohup命令
(1)如下代码:
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
return 0;
}
(2)当我们正常编译运行后,此程序默认是在前台进行的:
(3)我们可以使用如下的指令将其变成后台进程:
nohup ./a.out &
此时会发现我nohup.out文件的大小在不断增大,使用如下命令帮助我们观察现象:
[xiaomaker@VM-28-13-centos test_01_31]$ ps axj | head -1 && ps ajx | grep a.out
[xiaomaker@VM-28-13-centos test_01_31]$ ps ajx | grep 21763
(4)通过测试可以看出nohup进程当前正在运行,且PID和PGID是一样的,自成进程组,它所属的会话是21763(属于bash),此进程依旧是在本会话内部,并非是守护进程,但是已经很接近了。使用nohup命令可以让此进程不受用户退出和登陆的影响,已经是后台进程。其实也算是守护进程了。如下我们退出linux,重新登陆,再次执行刚才的ps命令:
9. TCP协议通讯流程
(1)下图是基于TCP协议的客户端/服务器程序的一般流程:
(2)服务器初始化:
- 调用socket,创建文件描述符;
- 调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败;
- 调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备;
- 调用accecpt, 并阻塞,等待客户端连接过来;
(3)建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程,通常称为三次握手;
(4)数据传输的过程:
- 建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回,发送下一条请求,如此循环下去;
(5)断开连接的过程:
- 如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后。会回应一个ACK,同时read会返回0 (第二次);
- read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次)。
- 客户端收到FIN,再返回一个ACK给服务器(第四次)。
这个断开连接的过程,通常称为四次挥手;