目录
一.网络知识
1.网络通信
2.端口号
(1)介绍
(2)端口号和进程ID
3.TCP协议
4.UDP协议
5.网络字节序
二. socket编程接口
1.socket常见API
2.sockaddr结构
(1)sockaddr结构
(2)sockaddr_in结构
(3)in_addr结构
三.实现一个UDP网络程序
1.Makefile
2.Log.hpp
3.udpServer.cc
4.udpClient.cc
四.地址转换函数和inet_ntoa
1.地址转换函数
2.inet_ntoa
五.守护进程
六.实现一个TCP网络程序(守护进程化)
1.Lock.hpp
2.Task.hpp
3.ThreadPool.hpp
4.log.hpp
5.util.hpp
6.clientTcp.cc
7.serverTcp.cc
8.daemonize.hpp
七.TCP协议通讯流程
1.服务器初始化
2.建立连接的过程
3.数据传输的过程
4.断开连接的过程
前言:从这一篇开始,细致的介绍网络,学会使用网络编程
一.网络知识
1.网络通信
主机间通信的目的本质是:在各自的主机上的两个进程在互相交互数据。
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方。
在进行通信的时候,不仅仅要考虑两台主机间互相交互数据。
本质上讲,进行数据交互的时候,是用户和用户在进行交互。用户的身份,通常是用程序体现的。程序一定是在运行中的 ---- 进程。
IP ---- 确保主机的唯一性
端口号(port):确保该主机上的进程的唯一性
IP:PORT = 标识互联网中唯一的一个进程。 ---- socket
网络通信的本质:也是进程间通信。
2.端口号
(1)介绍
① 端口号是一个2字节16位的整数;
② 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
③ IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
④ 一个端口号只能被一个进程占用
(2)端口号和进程ID
进程pid是可以表示唯一一个进程的,此处端口号也是唯一表示一个进程。那么为什么不用进程ID表示,而用端口号呢?
使用端口号,可以解耦,并且一个进程可能会绑定多个端口号,但是一个进程只有一个进程ID。看到端口号就可以知道这个是属于网络的。(举个例子:就比如学生都有身份证号,并且是唯一的,但是学校中又使用学号,也是唯一的,但是如果身份证不用了,也不会影响学校,这就完成了解耦,同时学校使用学号,会更方便的对学生进行管理,同时如果有学号就说明是属于这个学校的,但是身份证号是无法区分是不是这个学校的。)
3.TCP协议
① 传输层协议
② 有连接
③ 可靠传输
④ 面向字节流
4.UDP协议
① 传输层协议
② 无连接
③ 不可靠传输
④ 面向数据报
5.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
① 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
② 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
③ 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
④ TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
⑤ 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
⑥ 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
① 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
② 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
③ 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
④ 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
二. socket编程接口
1.socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
2.sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要说的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
① 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结构
(2)sockaddr_in结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
(3)in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
三.实现一个UDP网络程序
1.Makefile
.PHONY:all
all:udpClient udpServer
udpClient: udpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpClient udpServer
2.Log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap); // ap = NULL
FILE *out = (level == FATAL) ? stderr : stdout;
fprintf(out, "%s | %u | %s | %s\n",
log_level[level],
(unsigned int)time(nullptr),
name == nullptr ? "unknow" : name,
logInfo);
}
3.udpServer.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
static void Usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}
/// @brief 我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:
UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
{
}
~UdpServer()
{
}
public:
void init()
{
// 1. 创建socket套接字
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
if (sockfd_ < 0)
{
logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
exit(1);
}
logMessage(DEBUG, "socket create success: %d", sockfd_);
// 2. 绑定网络信息,指明ip+port
// 2.1 先填充基本信息到 struct sockaddr_in
struct sockaddr_in local; // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
bzero(&local, sizeof(local)); // memset
// 填充协议家族,域
local.sin_family = AF_INET;
// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
local.sin_port = htons(port_);
// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip
// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
// 2.2 bind 网络信息
if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
{
logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
exit(2);
}
logMessage(DEBUG, "socket bind success: %d", sockfd_);
// done
}
void start()
{
// 服务器设计的时候,服务器都是死循环
char inbuffer[1024]; //将来读取到的数据,都放在这里
char outbuffer[1024]; //将来发送的数据,都放在这里
while (true)
{
struct sockaddr_in peer; //输出型参数
socklen_t len = sizeof(peer); //输入输出型参数
// demo2
// UDP无连接的
// 对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数
ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
inbuffer[s] = 0; //当做字符串
}
else if (s == -1)
{
logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);
continue;
}
// 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
std::string peerIp = inet_ntoa(peer.sin_addr); //拿到了对方的IP
uint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的port
checkOnlineUser(peerIp, peerPort, peer); //如果存在,什么都不做,如果不存在,就添加
// 打印出来客户端给服务器发送过来的消息
logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);
// for(int i = 0; i < strlen(inbuffer); i++)
// {
// if(isalpha(inbuffer[i]) && islower(inbuffer[i])) outbuffer[i] = toupper(inbuffer[i]);
// else outbuffer[i] = toupper(inbuffer[i]);
// }
messageRoute(peerIp, peerPort,inbuffer); //消息路由
// 线程池!
// sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);
// demo1
// logMessage(NOTICE, "server 提供 service 中....");
// sleep(1);
}
}
void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
{
std::string key = ip;
key += ":";
key += std::to_string(port);
auto iter = users.find(key);
if(iter == users.end())
{
users.insert({key, peer});
}
else
{
// iter->first, iter->second->
// do nothing
}
}
void messageRoute(std::string ip, uint32_t port, std::string info)
{
std::string message = "[";
message += ip;
message += ":";
message += std::to_string(port);
message += "]# ";
message += info;
for(auto &user : users)
{
sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
}
}
private:
// 服务器必须得有端口号信息
uint16_t port_;
// 服务器必须得有ip地址
std::string ip_;
// 服务器的socket fd信息
int sockfd_;
// onlineuser
std::unordered_map<std::string, struct sockaddr_in> users;
};
// struct client{
// struct sockaddr_in peer;
// uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }
// ./udpServer port [ip]
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if (argc == 3)
{
ip = argv[2];
}
UdpServer svr(port, ip);
svr.init();
svr.start();
return 0;
}
4.udpClient.cc
#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
struct sockaddr_in server;
static void Usage(std::string name)
{
std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}
void *recverAndPrint(void *args)
{
while (true)
{
int sockfd = *(int *)args;
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
}
// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 根据命令行,设置要访问的服务器IP
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2. 创建客户端
// 2.1 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
// 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定
// 如果我非要自己bind呢?可以!但是严重不推荐!
// 所有的客户端软件 <-> 服务器 通信的时候,必须得有 client[ip:port] <-> server[ip:port]
// 为什么呢??client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了
// 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!
// 2.2 填写服务器对应的信息
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
pthread_t t;
pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
// 3. 通讯过程
std::string buffer;
while (true)
{
std::cerr << "Please Enter# ";
std::getline(std::cin, buffer);
// 发送消息给server
sendto(sockfd, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
}
close(sockfd);
return 0;
}
本地通信测试:
这个也是可以远程通信的,通过sz udpClient,然后发布该文件,别人再rz -e该文件,因为默认情况是没有可执行权限的,再chmod +x udpClient,这时再找到自己云服务器的公网ip,别人./udpClient 公网ip 8080,就可以在自己的服务器接收到别人发的信息了。
也可以在Window中创建一个可以链接上Linux的可执行程序,那么在Window中输入的信息,就可以显式在Linux中。
还可以把广播过来的信息放入一个fifo里,这个fifo文件就是一个聊天室。
这里可以看到,输入在udpClient中的信息都显示在了fifo的管道文件中。同时也有对应输出信息那个人的ip和端口号。
四.地址转换函数和inet_ntoa
1.地址转换函数
本篇只介绍基于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
示例:
2.inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
① 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
② 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
③ 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
多线程调用inet_ntoa代码示例如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
}
return NULL;
}
void *Func2(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr2: %s\n", ptr);
}
return NULL;
}
int main()
{
pthread_t tid1 = 0;
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
五.守护进程
一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程)的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。
构建一个会话,必须要有一个前台进程组(而且任何时刻,只能有一个前台进程组),给我们加载bash,有0个或者多个后台进程组。
加载的bash构建了一个会话,自己是会话前台进程组,后续如果我们自己再新启动进程(启动进程组),依旧属于bash自己的会话。
因此在命令行中启动一个进程,现在就可以叫做在会话中启动一个进程组,来完成某中任务。
所有会话内的进程fork创建子进程,一般而言依旧属于当前会话。
在登录的状态时,新起了一个网络服务器,创建好之后,再派生的子进程也属于当前会话,所以我们就不能让这个网络服务器属于这个会话内容,要不然它会受到用户的登录和注销的影响。
所以当我们有个网络服务的时候,应该脱离这个会话,让它独立的在计算机里自成进程组,自成新会话。这样在两个用户同时登录的时候,形成的两个会话是独立的,在操作各自的bash不会互相影响。
像这种自成进程组,自成新会话,而且周而复始进行的进程称为守护进程(精灵进程)。
如何自己形成呢?
必须调用一个函数setsid():将调用进程设置称为独立的会话
进程组的组长不应调用setsid()
那么如何让'我'不成为组长呢?可以让'我'成为进程组内的第二个进程。常规做法:fork()子进程,子进程就不是组长进程了,它就可以成功调用setsid()。
server端一直在写,client端关闭了,server端就会被终止(server会收到SIGPIPE信号终止)
因此,必做的:
if(fork() > 0) exit(0)
setsid();
选做的:
忽略SIGPIPE信号
更改进程的工作目录,通过chdir更改
方法:
1.close(0, 1, 2) 【很少有这么做的,不推荐】
2.打开 /dev/null ,并且进行对0, 1, 2重定向。 (/dev/null 类似于Linux下的一个"垃圾桶 or 文件黑洞",凡是从/dev/null里面读写一概被丢弃)
六.实现一个TCP网络程序(守护进程化)
1.Lock.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&lock_, nullptr);
}
void lock()
{
pthread_mutex_lock(&lock_);
}
void unlock()
{
pthread_mutex_unlock(&lock_);
}
~Mutex()
{
pthread_mutex_destroy(&lock_);
}
private:
pthread_mutex_t lock_;
};
class LockGuard
{
public:
LockGuard(Mutex *mutex) : mutex_(mutex)
{
mutex_->lock();
std::cout << "加锁成功..." << std::endl;
}
~LockGuard()
{
mutex_->unlock();
std::cout << "解锁成功...." << std::endl;
}
private:
Mutex *mutex_;
};
2.Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"
class Task
{
public:
//等价于
// typedef std::function<void (int, std::string, uint16_t)> callback_t;
using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
int sock_; // 给用户提供IO服务的sock
uint16_t port_; // client port
std::string ip_; // client ip
callback_t func_; // 回调方法
public:
Task():sock_(-1), port_(-1)
{}
Task(int sock, std::string ip, uint16_t port, callback_t func)
: sock_(sock), ip_(ip), port_(port), func_(func)
{}
void operator () ()
{
logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\
pthread_self(), ip_.c_str(), port_);
func_(sock_, ip_, port_);
logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\
pthread_self(), ip_.c_str(), port_);
}
~Task()
{}
};
3.ThreadPool.hpp
#pragma once
#include <iostream>
#include <cassert>
#include <queue>
#include <memory>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "Lock.hpp"
using namespace std;
int gThreadNum = 15;
template <class T>
class ThreadPool
{
private:
ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false)
{
assert(threadNum_ > 0);
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
ThreadPool(const ThreadPool<T> &) = delete;
void operator=(const ThreadPool<T>&) = delete;
public:
static ThreadPool<T> *getInstance()
{
static Mutex mutex;
if (nullptr == instance) //仅仅是过滤重复的判断
{
LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁
if (nullptr == instance)
{
instance = new ThreadPool<T>();
}
}
return instance;
}
//类内成员, 成员函数,都有默认参数this
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
// prctl(PR_SET_NAME, "follower"); // 更改线程名称
while (1)
{
tp->lockQueue();
while (!tp->haveTask())
{
tp->waitForTask();
}
//这个任务就被拿到了线程的上下文中
T t = tp->pop();
tp->unlockQueue();
t(); // 让指定的先处理这个任务
}
}
void start()
{
assert(!isStart_);
for (int i = 0; i < threadNum_; i++)
{
pthread_t temp;
pthread_create(&temp, nullptr, threadRoutine, this);
}
isStart_ = true;
}
void push(const T &in)
{
lockQueue();
taskQueue_.push(in);
choiceThreadForHandler();
unlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
int threadNum()
{
return threadNum_;
}
private:
void lockQueue() { pthread_mutex_lock(&mutex_); }
void unlockQueue() { pthread_mutex_unlock(&mutex_); }
bool haveTask() { return !taskQueue_.empty(); }
void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
T pop()
{
T temp = taskQueue_.front();
taskQueue_.pop();
return temp;
}
private:
bool isStart_;
int threadNum_;
queue<T> taskQueue_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *instance;
// const static int a = 100;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
4.log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
#define LOGFILE "serverTcp.log"
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap); // ap = NULL
// 每次打开太麻烦
umask(0);
int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
assert(fd >= 0);
FILE *out = (level == FATAL) ? stderr : stdout;
dup2(fd, 1);
dup2(fd, 2);
fprintf(out, "%s | %u | %s | %s\n",
log_level[level],
(unsigned int)time(nullptr),
name == nullptr ? "unknow" : name,
logInfo);
fflush(out); // 将C缓冲区中的数据刷新到OS
fsync(fd); // 将OS中的数据尽快刷盘
close(fd);
// char *s = format;
// while(s){
// case '%':
// if(*(s+1) == 'd') int x = va_arg(ap, int);
// break;
// }
}
5.util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
6.clientTcp.cc
#include "util.hpp"
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!
volatile bool quit = false;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1. 创建socket SOCK_STREAM
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
// 2.1 先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 发起请求,connect 会自动帮我们进行bind!
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(CONN_ERR);
}
std::cout << "info : connect success: " << sock << std::endl;
std::string message;
while (!quit)
{
message.clear();
std::cout << "请输入你的消息>>> ";
std::getline(std::cin, message); // 结尾不会有\n
if (strcasecmp(message.c_str(), "quit") == 0)
quit = true;
ssize_t s = write(sock, message.c_str(), message.size());
if (s > 0)
{
message.resize(1024);
ssize_t s = read(sock, (char *)(message.c_str()), 1024);
if (s > 0)
message[s] = 0;
std::cout << "Server Echo>>> " << message << std::endl;
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
7.serverTcp.cc
#include "util.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "daemonize.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
class ServerTcp; // 申明一下ServerTcp
// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char inbuffer[BUFFER_SIZE];
while (true)
{
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串
if (s > 0)
{
// read success
inbuffer[s] = '\0';
if (strcasecmp(inbuffer, "quit") == 0)
{
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
// 可以进行大小写转化了
for (int i = 0; i < s; i++)
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
inbuffer[i] = toupper(inbuffer[i]);
}
logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
write(sock, inbuffer, strlen(inbuffer));
}
else if (s == 0)
{
// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
// s == 0: 代表对方关闭,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到这里,一定是client退出了,服务到此结束
close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
char command[BUFFER_SIZE];
while (true)
{
ssize_t s = read(sock, command, sizeof(command) - 1); //我们认为我们读到的都是字符串
if (s > 0)
{
command[s] = '\0';
logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);
// 考虑安全
std::string safe = command;
if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
{
break;
}
// 我们是以r方式打开的文件,没有写入
// 所以我们无法通过dup的方式得到对应的结果
FILE *fp = popen(command, "r");
if(fp == nullptr)
{
logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));
break;
}
char line[1024];
while(fgets(line, sizeof(line)-1, fp) != nullptr)
{
write(sock, line, strlen(line));
}
// dup2(fd, 1);
// dup2(sock, fp->_fileno);
// fflush(fp);
pclose(fp);
logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);
}
else if (s == 0)
{
// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭
// s == 0: 代表对方关闭,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到这里,一定是client退出了,服务到此结束
close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
class ThreadData
{
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
ServerTcp *this_;
public:
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts)
{
}
};
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1),
tp_(nullptr)
{
}
~ServerTcp()
{
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "socket: %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);
// 2. bind绑定
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind: %s", strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5 /*后面再说*/) < 0)
{
logMessage(FATAL, "listen: %s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
// 运行别人来连接你了
// 4. 加载线程池
tp_ = ThreadPool<Task>::getInstance();
}
// static void *threadRoutine(void *args)
// {
// pthread_detach(pthread_self()); //设置线程分离
// ThreadData *td = static_cast<ThreadData *>(args);
// td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
// delete td;
// return nullptr;
// }
void loop()
{
// signal(SIGCHLD, SIG_IGN); // only Linux
tp_->start();
logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接, accept 的返回值是一个新的socket fd ??
// 4.1 listenSock_: 监听 && 获取新的链接-> sock
// 4.2 serviceSock: 给用户提供新的socket服务
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5 提供服务, echo -> 小写 -> 大写
// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
// transService(serviceSock, peerIp, peerPort);
// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
// pid_t id = fork();
// assert(id != -1);
// if(id == 0)
// {
// close(listenSock_); //建议
// //子进程
// transService(serviceSock, peerIp, peerPort);
// exit(0); // 进入僵尸
// }
// // 父进程
// close(serviceSock); //这一步是一定要做的!
// 5.1 v1.1 版本 -- 多进程版本 -- 也是可以的
// 爷爷进程
// pid_t id = fork();
// if(id == 0)
// {
// // 爸爸进程
// close(listenSock_);//建议
// // 又进行了一次fork,让 爸爸进程
// if(fork() > 0) exit(0);
// // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收
// transService(serviceSock, peerIp, peerPort);
// exit(0);
// }
// // 父进程
// close(serviceSock); //这一步是一定要做的!
// // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态
// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
// assert(ret > 0);
// (void)ret;
// 5.2 v2 版本 -- 多线程
// 这里不需要进行关闭文件描述符吗??不需要啦
// 多线程是会共享文件描述符表的!
// ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// 5.3 v3 版本 --- 线程池版本
// 5.3.1 构建任务
// 5.3 v3.1
// Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// tp_->push(t);
// 5.3 v3.2
// Task t(serviceSock, peerIp, peerPort, transService);
// tp_->push(t);
// 5.3 v3.3
Task t(serviceSock, peerIp, peerPort, execCommand);
tp_->push(t);
// waitpid(); 默认是阻塞等待!WNOHANG
// 方案1
// logMessage(DEBUG, "server 提供 service start ...");
// sleep(1);
}
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 引入线程池
ThreadPool<Task> *tp_;
};
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"
<< std::endl;
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if (argc == 3)
ip = argv[2];
daemonize(); // 我们的进程就会成为守护进程
ServerTcp svr(port, ip);
svr.init();
svr.loop();
return 0;
}
8.daemonize.hpp
#pragma once
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize()
{
int fd = 0;
// 1. 忽略SIGPIPE
signal(SIGPIPE, SIG_IGN);
// 2. 更改进程的工作目录
// chdir();
// 3. 让自己不要成为进程组组长
if (fork() > 0)
exit(1);
// 4. 设置自己是一个独立的会话
setsid();
// 5. 重定向0,1,2
if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3
{
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6. 关闭掉不需要的fd
if(fd > STDERR_FILENO) close(fd);
}
}
七.TCP协议通讯流程
① tcp是面向链接的,client connect &&server accept
② tcp在建立链接的时候,采用的是三次握手,在断开链接的时候,采用的是四次挥手
③ connect发起三次挥手(client),close() client && closer() server -> close() 执行4次挥手中的2次
1.服务器初始化
① 调用socket, 创建文件描述符
② 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败
③ 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备
④ 调用accecpt, 并阻塞, 等待客户端连接过来
2.建立连接的过程
① 调用socket, 创建文件描述符;
② 调用connect, 向服务器发起连接请求;
③ connect会发出SYN段并阻塞等待服务器应答; (第一次)
④ 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
⑤ 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手
3.数据传输的过程
① 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据
② 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待
③ 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答
④ 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求
⑤ 客户端收到后从read()返回, 发送下一条请求,如此循环下去
4.断开连接的过程
① 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)
② 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)
③ read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
④ 客户端收到FIN, 再返回一个ACK给服务器(第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
① 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
② 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段