一、服务器的初始化
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
1.创建套接字
socket():
⭐参数介绍:
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可。
class TcpServer { public: TcpServer() : _sockfd(defaultsockfd) { } void Init() { _sockfd = socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) // 创建套接字失败 { lg(Fatal, "create socket, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, socket: %d", _sockfd); } ~TcpServer() { close(_sockfd); } private: int _sockfd; // 套接字 };
2.绑定端口号和ip
bind():
⭐参数介绍:
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号;
- 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们先来看看地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址 但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
- 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
- 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问 题;
我们的程序中对myaddr参数是这样初始化的:
- 1. 将整个结构体清零;
- 2. 设置地址类型为AF_INET;
- 3. 网络地址为0.0.0.0, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址;
class TcpServer { public: TcpServer(const uint16_t& port, const string& ip = defaultip) : _sockfd(defaultsockfd) , _port(port) , _ip(ip) { } void Init() { // 1.创建套接字 _sockfd = socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) // 创建套接字失败 { lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, socket: %d", _sockfd); //2.绑定端口号 // 使用这个结构体需要包头文件 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 转化为网络序列 local.sin_port = htons(_port); // 字符串转化为点时分形式的ip inet_aton(_ip.c_str(), &(local.sin_addr)); //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中 int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)); if(n < 0) { lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno)); exit(BindError); } } ~TcpServer() { close(_sockfd); } private: int _sockfd; // 套接字 uint16_t _port; // 端口号 string _ip; // ip地址 };
3.设置监听状态
listen():
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5);
- listen()成功返回0,失败返回-1;
#pragma onec #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cstdlib> #include <cstring> #include <arpa/inet.h> #include <netinet/in.h> #include "log.hpp" using namespace std; const int defaultsockfd = -1; const string defaultip = "0.0.0.0"; const int backlog = 10; // 但是一般不要设置的太大 Log lg; enum { UsageError = 1, SocketError = 2, BindError = 3, ListenError = 4 }; class TcpServer { public: TcpServer(const uint16_t& port, const string& ip = defaultip) : _sockfd(defaultsockfd) , _port(port) , _ip(ip) { } void Init() { // 1.创建套接字 _sockfd = socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) // 创建套接字失败 { lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, socket: %d", _sockfd); //2.绑定端口号 // 使用这个结构体需要包头文件 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 转化为网络序列 local.sin_port = htons(_port); // 字符串转化为点时分形式的ip inet_aton(_ip.c_str(), &(local.sin_addr)); //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中 int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)); if(n < 0) { lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg(Info, "bind socket success, socket: %d", _sockfd); // 3.设置监听状态 // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if (listen(_sockfd, backlog) < 0) { lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg(Info, "bind socket success, socket: %d", _sockfd); } ~TcpServer() { close(_sockfd); } private: int _sockfd; // 套接字 uint16_t _port; // 端口号 string _ip; // ip地址 };
4.设置服务器的端口号和ip
未来我们向让用户设置端口号,所以我们可以在main函数中借助命令行参数传递,对于ip我们就直接使用默认的ip参数"0.0.0.0"即可。
#include "TcpServer.hpp" #include <memory> void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl; } // ./tcpserver 8080 int main(int argc, char* argv[]) { if(argc != 2) { Usage(argv[0]); exit(UsageError); } uint16_t port = std::stoi(argv[1]); unique_ptr<TcpServer> tcpSvr(new TcpServer(port)); tcpSvr->Init(); //tcpSvr->Start(); return 0; }
现在我们就可以来测试一下啦!
二、服务器的运行
1.建立新链接
accept():
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
- 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。
⭐accept函数返回的套接字是什么?
调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。
⭐监听套接字与accept函数返回的套接字的作用:
- 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
- accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,类似于餐厅门口的迎宾,而真正为这些连接提供服务的套接字是accept函数返回的套接字,类似于餐厅的服务员,而不是监听套接字。
所以初始化TCP服务器时创建的套接字应该叫做监听套接字。为了表明寓意,我们将代码中套接字的名字由_sockfd改为_listensocket,这样写着更清楚明了。
#pragma onec #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cstdlib> #include <cstring> #include <arpa/inet.h> #include <netinet/in.h> #include "log.hpp" using namespace std; const int defaultsockfd = -1; const string defaultip = "0.0.0.0"; const int backlog = 10; // 但是一般不要设置的太大 Log lg; enum { UsageError = 1, SocketError = 2, BindError = 3, ListenError = 4 }; class TcpServer { public: TcpServer(const uint16_t& port, const string& ip = defaultip) : _listensocket(defaultsockfd) , _port(port) , _ip(ip) { } void Init() { // 1.创建套接字 _listensocket = socket(AF_INET, SOCK_STREAM, 0); if (_listensocket < 0) // 创建套接字失败 { lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, socket: %d", _listensocket); //2.绑定端口号 // 使用这个结构体需要包头文件 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 转化为网络序列 local.sin_port = htons(_port); // 字符串转化为点时分形式的ip inet_aton(_ip.c_str(), &(local.sin_addr)); //此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中 int n = bind(_listensocket, (const struct sockaddr*)&local, sizeof(local)); if(n < 0) { lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg(Info, "bind socket success, socket: %d", _listensocket); // 3.设置监听状态 // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if (listen(_listensocket, backlog) < 0) { lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg(Info, "bind socket success, socket: %d", _listensocket); } void Start() { lg(Info, "tcpServer is running...."); for (;;) { // 1. 获取新连接 - 知道客户端的ip地址和端口号 struct sockaddr_in client; socklen_t len = sizeof(client); // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾 // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员 int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len); if (sockfd < 0) { // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人 lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; // 所以这里使用continue } lg(Info, "get a new link..., sockfd: %d", sockfd); } } ~TcpServer() { close(_listensocket); } private: int _listensocket; // 套接字 uint16_t _port; // 端口号 string _ip; // ip地址 };
此时我们想来测试一下,但是我们的客户端还没有写,咋办呢?
telnet 127.0.0.1 8888
是一个网络诊断命令,它的作用是尝试通过Telnet协议连接到本地主机(本机)的8888端口。这里:
telnet
是命令本身,用于进行远程登录和管理。127.0.0.1
是IPv4环回地址,指向本地计算机。使用这个地址,你实际上是尝试连接到你自己的机器。8888
是端口号,许多应用程序和服务会监听特定的端口来接收数据。8888是一个常见的测试或备用端口。
上一个知识点我们提到udp它是不能绑定我们云服务器的公网ip的,但是绑定本地环回127.0.0.1可以的,我们看看tcp可不可以。
答案是也不可以绑定我们云服务器的公网ip的。那么绑定本地环回127.0.0.1可以嘛?
所以为了能连接我们的与服务器的公网ip,我们将服务器的ip设置为0.0.0.0,这就意味着该tcp服务器可以读取服务器任何一个ip,保证一定能连接上我们的服务器。
2.进行通信
建立通信我们首先就要获取到给服务器发送请求的端口号和ip地址
我们来测试一下,看看此时能不能接收到客户端的端口号和ip地址。
此时我们就能知道我们确实有连接到我们的服务器了。
⭐注意:我们使用的云服务默认是将端口号禁用了,我们需要在云服务器后台将我们的端口号取消禁用,否则的话我们只能在本地通信,我们可以使用windows连接一下服务器,看看出现上面情况。
所以就需要前往服务器进行安全组的设置。
随后我们再来测试一下哈。
同时我们这里还有一个问题,我们之前的端口号和网络序列都要转成网络序列,我们都使用了相应的接口,但是为什么用户发送来的数据,我们不需要手动调用接口转化为网络序列呢?在使用套接字通信的时候,会默认将用户发送来的数据转化成网络序列,而端口号和网络序列比较特殊,需要写给操作系统的,所以需要我们自己去转的。现在我们就可以进行通信了,由于tcp是基于数据流的,所以直接使用write和read接口即可。
void Service( int sockfd,const string& ip,const uint16_t& port) { char buffer[4096]; while(true) { // 读取用户发送的请求 ssize_t n = read(sockfd, buffer, sizeof(buffer)); if(n > 0) { buffer[n] = '\0'; cout << "client says: " << buffer << endl; string echo_string = "tcpserver echo# "; echo_string += buffer; write(sockfd, echo_string.c_str(), echo_string.size()); } } }
我们来运行一下:
此时我们发现就可以通信啦!!!但是上面的客户端不是我们写的,我们自己来写一个。
三、客户端的初始化
1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { cerr << "socket error" << endl; }
2.绑定端口号和ip
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
注意:
- 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动 多个客户端, 就会出现端口号被占用导致不能正确建立连接;
- 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动 服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦;
测试多个连接的情况
再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信. 分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接 受新的请求. 我们当前的这个TCP, 只能处理一个连接, 这是不科学的.后面我们会改成多线程版本的。
3.连接服务器
connect:
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1;
#include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> using namespace std; 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]); int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cerr << "socket error" << endl; } // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求 // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行 // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了! // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行 // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定 // 可是客户端此时不知道服务器的ip和端口号 // 使用命令行参数来解决 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)); int n = connect(sockfd, (const struct sockaddr*)&server, sizeof(server)); if(n < 0) { cerr << "connect error..." << endl; exit(2); } close(sockfd); return 0; }
4.给服务端发送信息
string message; while (true) { cout << "Please Enter# "; getline(cin, message); // 发送数据 int n = write(sockfd, message.c_str(), message.size()); // 读取数据 char inbuffer[4096]; n = read(sockfd, inbuffer, sizeof(inbuffer)); if (n > 0) { inbuffer[n] = 0; std::cout << inbuffer << std::endl; } }
此时我们的服务器和客户端就可以运行啦!
四、一些安全问题的处理
1.客户端直接退出,服务器处理
客户端直接退出,服务器会怎么样,服务器读取不到客户端发来的请求,此时read返回值为0,应该关闭网络文件描述符,剩余一种情况就是读取失败,我们直接打印警告日志信息。
void Service( int sockfd,const string& ip,const uint16_t& port) { char buffer[4096]; while(true) { // 读取用户发送的请求 ssize_t n = read(sockfd, buffer, sizeof(buffer)); if(n > 0) { buffer[n] = '\0'; cout << "client says: " << buffer << endl; 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", ip.c_str(), port, sockfd); break; } else { lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno)); break; } } }
运行一下:
2.单进程服务器,多个客户端连接
我们直接来看现象
然后我们让客户端分别写一条数据
我们发现此时客户端1能成功发送数据并接收数据,但是客户端2不行,随后我们来关闭客户端1.
此时客户端1一退出,客户端2立马连接,并且将刚刚的数据发送给服务器并成功并接收数据。为什么呢?因为此时我们的服务器是一个单进程,一个服务器为一个客户端提供服务时,此时由于我们写的是while循环,此时必须等客户端1退出,才能关闭文件描述符(让),然后这个服务器才会空闲,然后此时客户端2才能建立连接,服务器才会提供服务。此时就相当于餐厅只有一个服务员,等这个服务员服务好了客户端1,才能接待客户端2,所以我们这里需要改一下,让多个客户端共同使用。
void Start() { lg(Info, "tcpServer is running...."); for (;;) { // 1. 获取新连接 - 知道客户端的ip地址和端口号 struct sockaddr_in client; socklen_t len = sizeof(client); // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾 // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员 int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len); if (sockfd < 0) { // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人 lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; // 所以这里使用continue } lg(Info, "get a new link..., sockfd: %d", sockfd); // 2.根据新连接来进行通信 // 获取客户端的ip地址和端口号 uint16_t clientport = ntohs(client.sin_port); char clientip[32]; // 转化成主机序列 inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); cout << "clientport: " << clientport << ", clientip: " << clientip << endl; // 单进程版本 // Service(sockfd, clientip, clientport); // close(sockfd); // 多进程版本 pid_t id = fork(); if(id == 0) { // 子进程 // 子进程会继承父进程的文件描述符 close(_listensocket); if(fork() > 0) exit(0); // 此时子进程退出了 Service(sockfd, clientip, clientport); // 孙子进程执行 // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养 close(sockfd); exit(0); } // 父进程 // 文件描述符使用的是引用计数 // 关闭父进程的文件描述符不会影响子进程 close(sockfd); // 这里等待回收子进程的方式不能是阻塞等待 pid_t rid = waitpid(id, nullptr, 0); } }
此时我们再来测试一下:
此时我们无论来多少个客户端,我们的服务器都能解决!!!除了上面一种方法,我们还可以使用信号的方式,注意使用这个需要带上头文件<signal.h>
signal(SIGVHLD, SIG_IGN);
如果我们此时来了一大批客户,那么此时就会为每一个客户创建一个进程,而我们之前学过,创建子进程的成本是非常大的,所以此时我们可以使用多线程来解决。
#pragma onec #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cstdlib> #include <cstring> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/wait.h> #include <pthread.h> #include "log.hpp" using namespace std; const int defaultsockfd = -1; const string defaultip = "0.0.0.0"; const int backlog = 10; // 但是一般不要设置的太大 Log lg; enum { UsageError = 1, SocketError = 2, BindError = 3, ListenError = 4 }; class TcpServer; class ThreadData { public: ThreadData(int fd, const std::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) : _listensocket(defaultsockfd), _port(port), _ip(ip) { } void Init() { // 1.创建套接字 _listensocket = socket(AF_INET, SOCK_STREAM, 0); if (_listensocket < 0) // 创建套接字失败 { lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, listensocket: %d", _listensocket); // 2.绑定端口号 // 使用这个结构体需要包头文件 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 转化为网络序列 local.sin_port = htons(_port); // 字符串转化为点时分形式的ip inet_aton(_ip.c_str(), &(local.sin_addr)); // local.sin_addr.s_addr = INADDR_ANY; // 此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中 int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local)); if (n < 0) { lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg(Info, "bind socket success, listensocket: %d", _listensocket); // 3.设置监听状态 // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if (listen(_listensocket, backlog) < 0) { lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg(Info, "bind socket success, listensocket: %d", _listensocket); } void Start() { lg(Info, "tcpServer is running...."); for (;;) { // 1. 获取新连接 - 知道客户端的ip地址和端口号 struct sockaddr_in client; socklen_t len = sizeof(client); // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾 // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员 int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len); if (sockfd < 0) { // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人 lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; // 所以这里使用continue } lg(Info, "get a new link..., sockfd: %d", sockfd); // 2.根据新连接来进行通信 // 获取客户端的ip地址和端口号 uint16_t clientport = ntohs(client.sin_port); char clientip[32]; // 转化成主机序列 inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); cout << "clientport: " << clientport << ", clientip: " << clientip << endl; // 单进程版本 // Service(sockfd, clientip, clientport); // close(sockfd); // 多进程版本 // pid_t id = fork(); // if(id == 0) //{ // 子进程 // 子进程会继承父进程的文件描述符 // close(_listensocket); // if(fork() > 0) exit(0); // 此时子进程退出了 // Service(sockfd, clientip, clientport); // 孙子进程执行 // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养 // close(sockfd); // exit(0); //} // 父进程 // 文件描述符使用的是引用计数 // 关闭父进程的文件描述符不会影响子进程 // close(sockfd); // 这里等待回收子进程的方式不能是阻塞等待 // pid_t rid = waitpid(id, nullptr, 0); // 多线程版本 ThreadData* td = new ThreadData(sockfd, clientip, clientport, this); pthread_t tid; pthread_create(&tid, nullptr, Rountine, td); // 这里不用join,因为它是阻塞等待 // pthread_join(tid, nullptr); } } static void* Rountine(void* args) { pthread_detach(pthread_self()); // 设置分离状态 // 文件描述符共享,此时我们就不能关闭 // 多线程只拥有tcb ThreadData *td = static_cast<ThreadData *>(args); // static静态成员方法无法使用非静态成员方法和成员 // 1.将Service放到TcpServer类外 // 2.将当前对象的this指针传入 td->tsvr->Service(td->sockfd, td->clientip, td->clientport); delete td; return nullptr; } void Service(int sockfd, const string &ip, const uint16_t &port) { char buffer[4096]; while (true) { // 读取用户发送的请求 ssize_t n = read(sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; cout << "client says: " << buffer << endl; 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", ip.c_str(), port, sockfd); break; } else { lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno)); break; } } } ~TcpServer() { close(_listensocket); } private: int _listensocket; // 套接字 uint16_t _port; // 端口号 string _ip; // ip地址 };
此时我们再来运行一下:
此时我们是每次来一个客户端,然后就创建一个线程,也有点消耗,毕竟俺们每次都要创建,我们来写一个线程池,一开始我们就创建一大批进程,来一个客户端直接给它一个线程。并且我们上面的代码还存在一个问题,我们的服务是一直服务用户请求的,只要客户端不退,即使客户端不发信息,我们为该用户创建的线程也不会销毁,这样就会导致系统中的线程越来越多,直接来写代码。
#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 "log.hpp" #include <string.h> extern Log lg; using namespace std; class Task { public: Task(int sockfd, const std::string &clientip, const uint16_t &clientport) : sockfd_(sockfd), clientip_(clientip), clientport_(clientport) { } void run() { char buffer[4096]; // 读取用户发送的请求 ssize_t n = read(sockfd_, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; cout << "client says: " << buffer << endl; 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_); } else { lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno)); } close(sockfd_); } void operator()() { run(); } ~Task() { } private: int sockfd_; std::string clientip_; uint16_t clientport_; };
此时对于客户端,我们让它输入一次信息就不要再输入了,你要是再输入就需要重新连接服务器。
#include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> using namespace std; 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]); int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cerr << "socket error" << endl; } // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求 // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行 // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了! // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行 // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定 // 可是客户端此时不知道服务器的ip和端口号 // 使用命令行参数来解决 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)); int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server)); if (n < 0) { cerr << "connect error..." << endl; exit(2); } string message; cout << "Please Enter# "; getline(cin, message); // 发送数据 n = write(sockfd, message.c_str(), message.size()); // 读取数据 char inbuffer[4096]; n = read(sockfd, inbuffer, sizeof(inbuffer)); if (n > 0) { inbuffer[n] = 0; std::cout << inbuffer << std::endl; } close(sockfd); return 0; }
此时我们就不需要再创建线程了,并且用户发一次信息,服务处理完该线程就退了,用户如果还有就需要重新连接服务器。
3.服务端写入失败,写入需要判断
此时我们再来看看结果:
此时就符合我们的预期结果啦!
4.服务器未写,sockfd链接断开
当我们的服务器已经读到数据的时候,但是此时文件描述符的链接一键断开,此时再向这个失效的文件描述符写那么程序就会出问题,这个就相当于之前的管道,如果读端关掉,那么写端继续写当前进行就会收到SIGPIPE信号,将进程终止掉,此时程序就会出现SIGPIPE信号,我们直接将其直接终止。
5.客户端多次链接,服务器提供多次服务
然后我们来测试一下:
这里我们再次链接的到时候为什么失败了呢?这是因为我们第一次发出请求的时候,服务器处理完了之后就将文件描述符关掉了,而链接的时候使用的文件描述符已经被关了,此时会链接失败,所以要想再次链接,就必须要再次创建套接字。
此时就解决问题啦!
6.客户端未发送,服务器断开
当客户端向服务器写的时候,此时服务器断开了,此时我们希望能够重新连接服务器,如果重连5次还没有连接上,用户也离线。
#include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> using namespace std; 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]); // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求 // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行 // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了! // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行 // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定 // 可是客户端此时不知道服务器的ip和端口号 // 使用命令行参数来解决 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 cnt = 5; // 重连的次数 bool isreconnect = false; int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cerr << "socket error" << endl; } do { int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server)); if (n < 0) { isreconnect = true; cnt--; std::cerr << "connect error..., reconnect: " << cnt << std::endl; close(sockfd); sleep(2); } else { break; } } while (cnt && isreconnect); if (cnt == 0) { std::cerr << "user offline..." << std::endl; break; } string message; cout << "Please Enter# "; getline(cin, message); // 发送数据 n = write(sockfd, message.c_str(), message.size()); if (n < 0) { isreconnect = true; std::cerr << "write error..." << std::endl; continue; } // 读取数据 char inbuffer[4096]; n = read(sockfd, inbuffer, sizeof(inbuffer)); if (n > 0) { inbuffer[n] = 0; std::cout << inbuffer << std::endl; } close(sockfd); } return 0; }
运行结果:
上面这个情况就是我们打游戏的时候,我们的网断开了,就相当于连接不上服务器了,此时就会多次连接,如果连接了很多次都没有连接上,此时游戏也就会退出啦。
五、英译汉服务器
makefile
.PHONY:all all:tcpserver tcpclient tcpserver:Main.cc g++ -o $@ $^ -std=c++11 -lpthread tcpclient:TcpClient.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f tcpserver tcpclient
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: 它
Main.cc
#include "TcpServer.hpp" #include <memory> void Usage(std::string proc) { std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl; } // ./tcpserver 8080 int main(int argc, char* argv[]) { if(argc != 2) { Usage(argv[0]); exit(UsageError); } uint16_t port = std::stoi(argv[1]); unique_ptr<TcpServer> tcpSvr(new TcpServer(port)); tcpSvr->Init(); tcpSvr->Start(); return 0; }
Task.hpp
#pragma once #include <iostream> #include <string> #include "log.hpp" #include "Init.hpp" #include <string.h> extern Log lg; Init init; using namespace std; class Task { public: Task(int sockfd, const std::string &clientip, const uint16_t &clientport) : sockfd_(sockfd), clientip_(clientip), clientport_(clientport) { } void run() { char buffer[4096]; // 读取用户发送的请求 ssize_t n = read(sockfd_, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; cout << "client keys: " << buffer << endl; string echo_string = init.translation(buffer); int 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_); } else { lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno)); } close(sockfd_); } void operator()() { run(); } ~Task() { } private: int sockfd_; std::string clientip_; uint16_t clientport_; };
Init.pp
#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 = ":"; 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; };
log.hpp
#pragma once #include <iostream> #include <string> #include "log.hpp" #include "Init.hpp" #include <string.h> extern Log lg; Init init; using namespace std; class Task { public: Task(int sockfd, const std::string &clientip, const uint16_t &clientport) : sockfd_(sockfd), clientip_(clientip), clientport_(clientport) { } void run() { char buffer[4096]; // 读取用户发送的请求 ssize_t n = read(sockfd_, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; cout << "client keys: " << buffer << endl; string echo_string = init.translation(buffer); int 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_); } else { lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno)); } close(sockfd_); } void operator()() { run(); } ~Task() { } private: int sockfd_; std::string clientip_; uint16_t clientport_; };
TcpServer.hpp
#pragma onec #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <cstdlib> #include <cstring> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/wait.h> #include <signal.h> #include <pthread.h> #include "log.hpp" #include "ThreadPool.hpp" #include "Task.hpp" using namespace std; const int defaultsockfd = -1; const string defaultip = "0.0.0.0"; const int backlog = 10; // 但是一般不要设置的太大 extern Log lg; enum { UsageError = 1, SocketError = 2, BindError = 3, ListenError = 4 }; class TcpServer; class ThreadData { public: ThreadData(int fd, const std::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) : _listensocket(defaultsockfd), _port(port), _ip(ip) { } void Init() { // 1.创建套接字 _listensocket = socket(AF_INET, SOCK_STREAM, 0); if (_listensocket < 0) // 创建套接字失败 { lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno)); exit(SocketError); } lg(Info, "create socket success, listensocket: %d", _listensocket); // 2.绑定端口号 // 使用这个结构体需要包头文件 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; // 转化为网络序列 local.sin_port = htons(_port); // 字符串转化为点时分形式的ip inet_aton(_ip.c_str(), &(local.sin_addr)); // local.sin_addr.s_addr = INADDR_ANY; // 此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中 int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local)); if (n < 0) { lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno)); exit(BindError); } lg(Info, "bind socket success, listensocket: %d", _listensocket); // 3.设置监听状态 // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态 if (listen(_listensocket, backlog) < 0) { lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno)); exit(ListenError); } lg(Info, "bind socket success, listensocket: %d", _listensocket); } void Start() { signal(SIGPIPE, SIG_IGN); ThreadPool<Task>::GetInstance()->Start(); lg(Info, "tcpServer is running...."); for (;;) { // 1. 获取新连接 - 知道客户端的ip地址和端口号 struct sockaddr_in client; socklen_t len = sizeof(client); // _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾 // sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员 int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len); if (sockfd < 0) { // 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人 lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //? continue; // 所以这里使用continue } lg(Info, "get a new link..., sockfd: %d", sockfd); // 2.根据新连接来进行通信 // 获取客户端的ip地址和端口号 uint16_t clientport = ntohs(client.sin_port); char clientip[32]; // 转化成主机序列 inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); cout << "clientport: " << clientport << ", clientip: " << clientip << endl; // 单进程版本 // Service(sockfd, clientip, clientport); // close(sockfd); // 多进程版本 // pid_t id = fork(); // if(id == 0) //{ // 子进程 // 子进程会继承父进程的文件描述符 // close(_listensocket); // if(fork() > 0) exit(0); // 此时子进程退出了 // Service(sockfd, clientip, clientport); // 孙子进程执行 // 对于孙子进程,它的父进程已经退出了,此时孙子进程被系统领养 // close(sockfd); // exit(0); //} // 父进程 // 文件描述符使用的是引用计数 // 关闭父进程的文件描述符不会影响子进程 // close(sockfd); // 这里等待回收子进程的方式不能是阻塞等待 // pid_t rid = waitpid(id, nullptr, 0); // 多线程版本 // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this); // pthread_t tid; // pthread_create(&tid, nullptr, Rountine, td); // 这里不用join,因为它是阻塞等待 // pthread_join(tid, nullptr); // 线程池版本 Task t(sockfd, clientip, clientport); // 单例模式 ThreadPool<Task>::GetInstance()->Push(t); } } //static void* Rountine(void* args) //{ // pthread_detach(pthread_self()); // 设置分离状态 // 文件描述符共享,此时我们就不能关闭 // 多线程只拥有tcb //ThreadData *td = static_cast<ThreadData *>(args); // static静态成员方法无法使用非静态成员方法和成员 // 1.将Service放到TcpServer类外 // 2.将当前对象的this指针传入 //td->tsvr->Service(td->sockfd, td->clientip, td->clientport); //delete td; //return nullptr; //} ~TcpServer() { close(_listensocket); } private: int _listensocket; // 套接字 uint16_t _port; // 端口号 string _ip; // ip地址 };
TcpClient.cc
#include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> using namespace std; 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]); // 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求 // tcp客户端要不要bind?一定要显示绑定只不过不需要用户显示的bind!一般有OS自由随机选择! // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此! // 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行 // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! // 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了! // 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行 // 系统什么时候给我bind呢?首次发送connect的时候,进行自动随机绑定 // 可是客户端此时不知道服务器的ip和端口号 // 使用命令行参数来解决 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 cnt = 5; // 重连的次数 bool isreconnect = false; int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { cerr << "socket error" << endl; } do { int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); if (n < 0) { isreconnect = true; cnt--; std::cerr << "connect error..., reconnect: " << cnt << std::endl; sleep(2); } else { break; } } while (cnt && isreconnect); if (cnt == 0) { std::cerr << "user offline..." << std::endl; break; } string message; cout << "Please Enter# "; getline(cin, message); // 发送数据 int n = write(sockfd, message.c_str(), message.size()); if (n < 0) { isreconnect = true; std::cerr << "write error..." << std::endl; continue; } // 读取数据 char inbuffer[4096]; n = read(sockfd, inbuffer, sizeof(inbuffer)); if (n > 0) { inbuffer[n] = 0; std::cout << inbuffer << std::endl; } close(sockfd); } return 0; }
服务端:
客户端:
然后我们再来测试一下如果服务器断开了我们还能不能重新连上。
我们发现此时不能重现连接成功,这是因为我们的端口号不能重现启动,所以我们要在服务器端加两句代码。
int opt = 1; setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)
此时我们来看看运行结果:
六、前台和后台进程
可是万一有一天我们不小心将我们的xshell关掉了呢?此时服务器就断开了,我们想xshell关掉了服务器依然能跑,要做到这个,我们要理解前台进程和后台进程,先来测试一下它们的特点。
#include <iostream> #include <string> #include <unistd.h> int main() { while(true) { std::cout << "hello ...." << std::endl; sleep(1); } return 0; }
前台进程:
后台进程:
前台进程:
- 直接交互:前台进程直接与用户交互,意味着用户可以通过命令行与这些进程进行输入和输出操作。
- 终端阻塞:当一个进程在前台运行时,它通常会阻塞用户终端,直到该进程完成或被挂起。这意味着用户不能在同一终端启动其他需要交互的进程。
- 数量限制:在一个会话中,同时只能有一个进程组在前台运行,尽管这个进程组内可能包含多个进程。
后台进程:
- 非交互式运行:后台进程在后台运行,不直接与用户交互,即使用户没有主动与其互动,也能持续执行任务。
- 不阻塞终端:用户可以在后台进程运行的同时,在同一个终端上执行其他命令或启动其他进程,因为它不阻塞用户界面。
- 启动方式:可以通过在命令末尾添加符号&来将进程置于后台启动。
此时我们可以把进程运行的结果重定向到文件中,如果交给我们的前台进程,那么此时只能执行一个写入文件操作,因为我们的前台进程只有一个,但是后台进程有多个,我们可以交给后台进程,并且还能通过jobs来查看后台进程。
如果我们向终止任务,可以使用fg 任务号将这个任务提到前台进程,但是此时bash就会变成后台进程,然后将它干掉即可。如果我们不想干掉,想重新仍会后台进程呢?
此时我们暂停前台进程,系统要不要把bash提到前台进程,把暂停的进程提到后台进程,要的,bash必须变成前台进程,所以在命令行中,前台进程一定存在。
随后我们再来理解一下后台进程。
每次登录的时候,我们的session都是不同的,所以session id也是不同的。
这里我们在开启两个终端。
如果我们的前台进程退了,开启的后台进程呢?它会自己退嘛?
此时我们发现后台进程也退出了,所以用户在退出的时候,会将自己启动的所有进程关掉,这就是注销,如果我们不想让我们的后台进程不随任何用户的登录或者退出受影响,此时我们就要守护进程化,我们将自成session自成进程组的进程,称为守护进程。
注意:如果调用进程不是一个进程组组长,则创建一个新的会话,但是我们怎么保证不是进程组组长的呢?我们可以创建一个子进程,然后父进程退出,此时的子进程就不是一个进程组组长,则创建一个新的会话,所以守护进程的本质,也是孤儿进程!但是该孤儿进程很坚强,它把自己设置成新的session,它就是一个独立的session,这样不隶属于任何用户登录和注销的影响。
#pragma onec #include <unistd.h> #include <iostream> #include <signal.h> #include <cstdlib> #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文件,它像一个垃圾桶 // 凡是向/dev/null文件里面写入的信息全部都会被丢弃 // 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); } }
随后我们将守护进程的代码加入服务器启动的最开始的地方。
随后我们开始运行一下:
并且此时我们能够确定我们的服务器的进程不隶属于bash的session,而是单成session。
bash的session id和我们的服务器的session id不一样哦!
此时我们能够发现我们已经将日志重定向到垃圾桶啦!随后我们再将我们的xshell关掉,然后我们再打开xshell,运行我们的服务器,看看结果:
此时只要我们一运行我们的客户端,就可以访问,真正做到了24小时提供服务,那我们怎么关掉我们的服务器呢?直接kill就行啦!!!
注意:为了标识守护进程,我们一般给文件名为末尾加上d。
如果我们想保留日志文件,那么我们就要传入写入的方式。
此时我们就能在日志里面看到该日志文件,但是守护进程还是太麻烦了,要自己来写,其实系统也为我们实现了。
这个函数接受两个整型参数:
nochdir:
- 如果
nochdir
参数为0,daemon()
函数将会把当前工作目录更改为根目录("/")。这是守护进程的标准行为,避免因当前工作目录被卸载而导致的问题。- 如果
nochdir
为非0值,则不改变当前工作目录。noclose:
- 如果
noclose
参数为0,daemon()
函数会关闭标准输入、标准输出和标准错误,并将它们都重定向到/dev/null
。这可以防止守护进程因为试图写入终端而阻塞或产生不必要的输出。- 如果
noclose
为非0值,标准输入、输出和错误保持不变。但通常情况下,为了确保守护进程的无终端运行,我们会选择关闭它们。
使用 daemon()
函数的基本步骤通常包括:
- 调用
fork()
创建子进程,父进程退出,这样新进程就不再与终端关联。 - 在子进程中调用
setsid()
成为新的会话领导并脱离控制终端。 - 调用
umask()
设置合适的权限掩码。 - 根据需要调用
chdir("/")
更改当前工作目录到根目录。 - 重定向标准输入、输出和错误流,或者通过
daemon()
函数自动处理。 - 继续执行守护进程的具体任务。
七、TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起;
- 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备; 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答;
- (第一次) 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (
- 第二次) 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务;
- 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据;
- 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN;(第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些 段,再比如read()返回0就表明收到了FIN段
谈恋爱例子
八、TCP全双工通信
全双工通信:
- 在全双工模式下,数据可以同时在两个方向上传输,即通信的双方能够同时进行发送和接收操作,互不影响。这就像两个人在打电话,双方可以同时说话和聆听,无需等待对方说完再回应。
半双工通信:
- 半双工模式允许数据在两个方向上传输,但不能同时进行。这意味着在任何给定的时间点,数据只能在一个方向流动。一旦一方开始发送数据,另一方就必须停止发送并转为接收模式,直到前一方发送完毕。这种模式类似于对讲机,使用者必须等待对方讲完并说“Over”后,才可开始自己的讲话。
我们为什么要讲这个呢?因为我们的tcp是全双工通信的,它是如何做到的呢?
每个TCP连接都有独立的发送缓冲区和接收缓冲区。这意味着一个端点可以在其发送缓冲区排队待发送的数据,同时从接收缓冲区读取对方发送过来的数据。这两个操作可以并发进行,从而实现了数据的双向同时传输,所以未来我们可以对一个套接字多进程的并发的读和写,但是两个线程不能同时读和同时写。