TCP协议通讯流程
tcp是面向连接的通信协议,在通信之前,需要进行3次握手,来进行连接的建立。
当tcp在断开连接的时候,需要释放连接,4次挥手
服务器初始化:
调用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给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
TCP 和 UDP 对比
可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 (对数据内部格式不关心,如客户端发了十几次消息,服务器一次性将这些数据读取)vs 数据报(如客户端发一次消息,服务器就收一条)
应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
协议
网络版本计算器
我们需要实现一个服务器版的计算器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最
后再把结果返回给客户端。
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转
化回结构体;
这个过程叫做 "序列化" 和 "反序列化"
不同平台当中存储的大小端方式不同或结构体内存对齐的方式不同,我们一般将数据转成序列化的数据,即转成一个字符串(字节流),以字符串(字节流)的方式向对方发送,对方收到字符串后进行解析,这个过程叫反序列化。
即客户端将数据序列化转为字节流,通过网络将字节流发送给服务器,服务器将字节流进行反序列化,并进行后续的操作。
结构化的数据是给上层业务使用的,而字节流式的数据的传输适合网络。
字段本身就是协议的一部分。
TCP是面向字节流的,如何保证服务器读到的内容是一个完整完善的请求呢?
UDP是面向数据报的,
TCP和UDP都是传输控制协议,TCP中有接收和发送缓冲区,当我们调用sned/write时,并不是把数据发送到网络或者对方的主机中,我们一般在发送的时候会手动设置一个缓冲区buffer,这个buffer是应用层的缓冲区,当我们发送数据的时候,其实是把buffer的数据拷贝到了“发送缓冲区”
在服务端调用recv/read只是把接收缓冲区的数据拷贝到了buffer人为设置的缓冲区当中
send/wirte/read/recv本质就是一个拷贝函数
至于数据什么时候发送给对方,发多少?出错了怎么办?这些都是由TCP决定的
一堆数据发送的次数和接收的次数没有任何关系,这就是面向字节流
我们能保证读到的数据是完整的吗?不能保证。
要解决这个问题,单纯的recv是无法解决这个问题的,我们需要对协议进一步定制
我们在报文中带一个length,length代表后续要计算的请求长度,即我们想要的报文是这样的
"length\r\nx_ op_ y_\r\n"这里\r\n是为了对这些字符进行区分,防止混在一起
可能会存在这样的问题9\r\n123 此时制度到了这些数据,我们需要等等,读到后面的789\r\n
在CalServer.cc里设置一个读取缓冲区
左边是CalServer.cc右边是
Decode函数是根据我们所定制的协议,把报文提取出来,把"length\r\nx_ op_ y_"v变为"x_ op_ y_"
同样的当服务器把数据返回的时候也要按照“length\r\ncode result\r\n”的方式发送出去。
我们写的服务器一般在启动的时候是这种格式./server 8080,那如果我们把终端关掉了,这个服务又该咋办?此时要用到进程守护,当终端退出时,守护进程继续工作,为客户端提供服务
守护进程
我们目前学到的服务器全部都是在前台运行的。
前台进程:和终端关联的进程叫前台进程。一个进程是否是前台进程取决于该进程能否正常获取你的输入,能否正常把输入的内容进行处理,如果能,则是前台进程。例如我们的bash本身就是前台进程。
当我们把服务器启动后,再输入linux命令,此时bash就不是前台进程,server是前台进程
任何xshell登录,只允许一个前台进程和多个后台进程
进程除了有自己的pid,ppid,还有自己的组id
在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash->即这三个进程可以用匿名管道来进行通信
下图中的sleep本身就是一个进程,三个sleep,三个进程,它们的父进程都是18699
而同时被创建的多个进程可以称为一个进程组的概念,组长一般就是第一个进程。
任何一次登录,登录的用户,需要有多个进程(组),来给这个用户提供服务如bash,用户也可以自己启动很多进程或者进程组,我们把给用户提供服务的进程或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
会话当中可以只有一个bash
当我们退出登录,理论上这些资源要释放掉,不同的操作系统都有不同的处理方式,所以说是理论上。
如果我们让某个进程自成一个会话,当推出登录时,该进程则不受影响,这种自成一个会话的进程被称作守护进程。
如何将一个进程自成会话
setsid
调用setsid函数,调用这个函数时,会让自己变成一个会话,自己自然也就是进程组的组长
如果设置成功,谁调用该函数,就返回谁的pid,如果失败了,返回-1
setsid要成功被调用,必须保证当前进程不是进程组的组长。
如何保证自己不是组长呢?可以通过fork保证自己不是组长。
如何在Linux中写一个让进程守护进程化的代码?
daemon
daemon用的并不多,一般是程序员自己写。
接下来我们自己写一个函数,让我们的进程调用这个函数,自动变成守护进程。
守护进程不能向显示器打印消息,一旦打印就会被暂停/终止
dev/null
linux中有一个文件叫dev/null,该文件有个特点,往该文件里面写的东西。会被自动全部丢弃,如果读取dev/null,我们什么都读不到也不会被阻塞,因此该文件可以随便进行操作,而不影响系统的正常运行
一个简单的守护进程
这个进程自成一派,TTY是表示这个进程和终端有没有关系,PPID是1,即守护进程的父进程就是1号进程,守护进程本质是孤儿进程的一种,守护进程自成会话
当我们不想自己写序列和反序列化,我们可以用json,json是网络通信的一种格式,可以帮助我们进行序列和反序列化,从而不写出冗余的代码。json是{key,value}式的结构,json也支持数组,数组里也是k,v模型,
在C++中我们要使用json,首先得安装一个jsconcpp这个库
CalClient.cc
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace ns_protocol;
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " serverIp serverPort\n"
<< std::endl;
}
// ./client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
Sock sock;
int sockfd = sock.Socket();
if (!sock.Connect(sockfd, server_ip, server_port))//客户端不需要绑定,直接连接即可
{
std::cerr << "Connect error" << std::endl;
exit(2);
}
bool quit = false;//表示当前是否退出
std::string buffer;
while (!quit)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req.x_ >> req.op_ >> req.y_;
// 2. 序列化
std::string s = req.Serialize();
// std::string temp = s;
// 3. 添加长度报头
s = Encode(s);
// 4. 发送给服务端
Send(sockfd, s);
// 5. 正常读取
while (true)
{
bool res = Recv(sockfd, &buffer);//把读进来的数据放到buffer中
if (!res)//读取失败了
{
quit = true;
break;
}
std::string package = Decode(buffer);//解析数据
if (package.empty())
continue;
Response resp;
resp.Deserialized(package);
std::string err;
switch (resp.code_)
{
case 1:
err = "除0错误";
break;
case 2:
err = "模0错误";
break;
case 3:
err = "非法操作";
break;
default:
std::cout << resp.x_ << resp.op_ << resp.y_ << " = " << resp.result_ << " [success]" << std::endl;
break;
}
if(!err.empty()) std::cerr << err << std::endl;
// sleep(1);
break;
}
}
close(sockfd);
return 0;
}
CalServer.cc
#include"TcpServer.hpp"
#include"Protocol.hpp"
#include"Daemon.hpp"
#include<memory>
#include<signal.h>
using namespace ne_protocol;//协议在这里
using namespace ns_tcpserver;
static void Usage(const std::string &process)
{
std::cout<<"\nUsage:"<<process<<" port\n"<<std::endl;
}
void debug(int sock)
{
std::cout<<"我是测试服务,得到的sock:"<<sock<<std::endl;
}
Response calculatorHelper(const Request& req)
{
//收到一个结构化的请求,返回一个结果化的响应
Response resp(0,0);
//结果和错误码初始化为0
switch(req.op_)
{
case'+':
resp.result_=req.x_+req.y_;
break;
case'-':
resp.result_=req.x_-req.y_;
break;
case'*':
resp.result_=req.x_*req.y_;
break;
default:
rep.code_=3;
break;
case'/':
if(req.y_==0) resp.code_=1;
else
resp.result_=req.x_/req.y_;
break;
case'%':
if(req.y_==0) resp.code_=2;
else
resp.result_=req.x_%req.y_;
break;
}
return resp;
}
//网络版本的计算器
void calculate(int sock)
{
std::string inbuffer;
while(true)
{
//1.先读取数据,对读取的数据大小不关心,只关心读取到的数据有没有出错
bool res=Recv(sock,&inbuffer);
//读取的时候把数据保存到inbuffer里
if(!res)
{
break;//读取失败
}
//2.协议解析,保证得到一个完整的报文,读取上来的数据都在inbuffer中
std::string package=Decode(inbuffer);//该函数在Protocol里面
//上面读取报文成功,这里是对报文进行解析
if(package.empty())
continue;//如果当前没有解析到报文
//走到这说明读到了报文,我们对报文进行操作
Request req;
//反序列化->字节流式的数据转为结构化的数据
req.Deserialized(str);//进行计算
Response resp=calculatorHelp(req);
//把计算结果结构化即序列化
respString=Encode(respString);
//
Send(sock,respString);
//把序列化的数据发送出去
//发出去的时候要按照"length\r\ncode result\r\n"的方式进行发送
}
void handler(int signo)
{
std::cout<<"get a signo:"<<signo<<std::endl;
exit(0);
}
//启动方式 ./Calserver port
int main(int argc,char *argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
signal(SIGPIPE,SIG_IGN);
//当客户端写入的时候突然关闭连接,会发生写入异常
//当写入发生异常的时候,我们对该信号进行捕捉并忽略
//一般经验:server在编写的时候,要有较为严谨的判断逻辑
//一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题
std::unique_ptr<ns_tcpserver::TcpServer> server(new TcpServer(atoi(argv[1])));
server->BindService(debug);//给服务器绑定一个服务
MyDaemon();
server->Start();
return 0;
}
Dameon.hpp
#pragma once
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
void MyDademon()
{
//1.忽略信号,如SIGPIPE,SIGCHLD
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
//2.不要让自己成为组长
if(fork()>0) exit(0);
//如果fork返回值大于0,就说明它是父进程,让父进程直接终止
//3.调用setsid
//走到这就是子进程了,子进程我们能保证它不是组长进程
setsid();//调用setsid即可
//4.标准输入,标准输出,标准错误的重定向
//守护进程不能直接向显示器打印消息,因为它是一个独立的进程,一旦打印就会被暂停
int devnull=open("dev/null",O_RDONLY|O_WRONLY);
if(devnull>0)//打开成功
{
//用dup2做重定向,int dup2(int oldfd,int newfd)把老的和新的文件描述符做重定向
//把oldfd重定向到newfd,
dup2(0,devnull);//把标准输入重定向到devnull当中
dup(1,devnull);
dup(2,devnull);
close(devnull);
}
}
Protocal.hpp
#pragma once
//定制协议
#include<iostream>
#include<string>
#include<cstring>
namespace ns_protocol
{
#define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n" //解析报文的时候要用到
#define SEP_LEN strlen(SEP) //不能用sizeof 会统计到\0
class Request
{
public:
//在这里进行序列化和反序列化
std::string Serialize()//序列化->将数据转为字符串
//我们把数据序列化成这个样子"x_ op_ y_"
{
#ifdef MYSELF
std::string str;
str=std::to_string(x_);
str+=SPACE;
str+=op_;
str+=SPACE;
str+=std::to_string(y_);
return str;
#else
std::cout<<"TODO"<<std::endl;
#endif
}
//"x_ op_ y_",必须采用这种方案
bool Deserialized(const std::string &str)//反序列化->将字符串转为数据
{
#ifdef MYSELF
std::ssize_t left=str.find(SPACE);
if(left==std::string::npos)
return false;//此时没有空格
std::size_t right=str.rfind(SPACE);
if(right==std::string::nops)
return false;
//如果有俩个空格,就开始截取子串
x_=atoi(str.substr(0,left).c_str());
y_=atoi(str.substr(right,SPACE_LEN).c_str());
if(left+SPACE_LEN>str.size())
return false;
op_=str[left+SPACE_LEN];
#else
std::cout<<"TODO"<<std::endl;
#endif
}
public:
Request(){}
Request(int x,int y,char op):x_(x),y_(y),op_(op){}
~Request(){}
public:
int x_;
int y_;
char op_;//加减乘除取模运算符
};
class Response
{
public:
//将状态码和结果进行序列化/反序列化
std::string Serialize()
{
#ifdef MYSELF
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
#else
Json::Value root;
root["code"] = code_;
root["result"] = result_;
root["xx"] = x_;
root["yy"] = y_;
root["zz"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
// "111 100"
bool Deserialized(const std::string &s)
{
#ifdef MYSELF
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
#else
Json::Value root;
Json::Reader reader;
reader.parse(s, root);
code_ = root["code"].asInt();
result_ = root["result"].asInt();
x_ = root["xx"].asInt();
y_ = root["yy"].asInt();
op_ = root["zz"].asInt();
return true;
#endif
}
public:
Responce(int result,int code):result_(result),code_(code){}
~Responce(){}
public:
int result_;//计算结果
int code_;//计算结果的状态码
//有可能会计算异常
//通过code判断是否计算正确
};
//读取格式"length\r\nx_ op_ y_\r\n"
//我们这里要返回一个完整的报文
//可能会存在这样的问题9\r\n123 此时制度到了这些数据
//我们需要等等,读到后面的789\r\n
bool Recv(int sock,std:;string *out)
{
char buffer[1024];
ssize_t s=recv(sock,buffer,sizeof(buffer)-1,0);
if(s>0) //读取成功
{
buffer[s]=0;
*out+=buffer;//把某次读取到的buffer全部保存到字符串当中
}
else if(s==0)
{
std::cout<<"client quit"<<std::endl;
return false;
}
else
{
std::cout<<"recv error"<<std::endl;//读取错误
return false;
}
return true;
}
void Send(int sock,const std::string str)
//发送数据
{
std::cout<<"send in"<<std::endl;
//表示当前进入到了sned的发送过程
int n=send(sock,str.c_str(),str.size(),0);
if(n<0)
{
std::cout<<"send errpr"<<std::endl;
}
}
std::string Decode(std::string &buffer)
{
std::size_t pos=buffer.find(SEP);//分隔符
if(pos==std:;string::npos) return"";
//如果当前没找到分隔符,说明可能只读取了length这一部分
//数据不完整,我们返回空串
int size=atoi(buffer.substr(0,pos).c_str());//报文长度 length
int surplus=buffer.size()-pos-2*SEP_LEN;//我们期望的正文长度
//buffer中去掉俩个\r\n和length之后的长度
if(surplus>=size)
//如果剩余的长度大于等于报文长度
//此时才至少具有一个合法完整的报文,我们进行提取
{
buffer.erase(0,pos+SEP_LEN);//把length\r\n先删掉
std::string s=buffer.substr(0,size);
//截取size个长度,此时已经拿到了报文
buffer.erase(0,size+SEP_LEN);//删掉末尾的\r\n
return s;
}
else
{
return "";
}
}
std::string EnCode(std::string &s)
{
std::string length=std::to_string(s.size());
//转为字符串长度
std::string new_package=length;
new_package+=SEP;//加分隔符
new_package+=s;
new_package+=SEP;
return new_package;
}
};
Sock.hpp
#pragma once
#include<iostream>
#include<cassert>
#include<pthread.h>
#include<sys/wait.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<netinet/in.h>
#include<string>
#include<signal.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<memory>
#include<cerrno>
#include<cstring>
#include"Log.hpp"
class Sock
{
private:
const static int gbacklog=20;//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
public:
Sock()
{}
int Socket()//创建套接字
{
int listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
//参数含义第一个:网络通信 第二个:流式通信
if(listensock<0)//创建套接字失败
{
logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
exit(2);
}
logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
return listensock;
}
void Bind(int sock,uint16_t port,std::string ip="0.0.0.0")
{
//bind目的是让IP和端口进行绑定
//我们需要套接字,和sockaddr(这个里面包含家族等名称)
//绑定——文件和网络
struct sockaddr_in local;
memset(&local,0,sizeof local);//初始化local
local.sin_family=AF_INET;
local.sin_port=htons(port);//端口号
local.sin_addr.s_addr=ip.empty()?INADDR_ANY:inet_addr(ip.c_str());
//IP地址,由于我们构造的时候是IP是个空的字符串
//所以我们可以绑定任意IP
//我们一般推荐绑定0号地址或特殊IP
//填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
if(bind(sock,(struct sockaddr*)&local,sizeof local)<0)
{
//走到这就绑定失败了,我们打印错误信息
logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
exit(3);
}
}
void Listen(int sock)//将套接字设置为listen状态
{
//因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
if(listen(sock,gbacklog)<0)
{
logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
exit(4);
}
logMessage(NORMAL,"init server success");
}
//一般经验
//const std;;string &::输入型参数
//std::string *:输出型参数
//std::string &:输入输出型参数
int Accept(int listensock,std::string *ip,uint16_t *port)//获取连接
{
//从套接字中获取到客户端相关的信息
struct sockaddr_in src;
socklen_t len=sizeof(src);
int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
if(servicesock<0)
{
//获取连接失败
logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
return -1;
}
if(port) *port=ntohs(src.sin_port);//如果port被设置,获取新的port
//客户端端口号在src
//由于是网络发送过来得套接字信息
//所以要把信息进行网络转主机
if(ip) *ip=inet_ntoa(src.sin_addr);//如果Ip被设置,获取新的Ip
//我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
//到这里我们拿到了IP和端口号
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)//进行连接
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
else return false;
}
~Sock(){}
};
TcpServer.hpp
#pragma once
#include"Sock.hpp"
#include<vector>
#include<functional>
#include<pthread.h>
namespace ns_tcpserver
{
using func_t=std::function<void(int)>;
class TcpServer;
class ThreadData//存放线程数据的类
{
public:
ThreadData(int sock,TcpServer *server):sock_(sock),server_(server){}
~ThreadData(){}
public:
int sock_;
TcpServer* server_;
};
class TcpServer
{
private:
static void* ThreadRoutine(void *args)
{
pthread_detach(pthread_self());//分离当前线程
//之后进行强制转换
ThreadData *td=static_cast<ThreadData* >(args);
//强制转换成我们想要的类型,这个类型是专门用来存储线程数据的
td->server_->Excute(td->sock_);//调用方法
close(td->sock_);//关闭套接字
delete td;//释放线程数据
return nullptr;
}
public:
TcpServer(const uint16_t &port,const std::string &ip="0.0.0.0")
{
listensock_=sock_.Socket();//创建套接字
sock_.Bind(listensock_,port,ip);//绑定
sock_.Listen(listensock_);
}
void BindService(func_t func)//给服务器绑定一个服务方法,即传一个函数类型的对象
{
//func_=func;
func_.push_back(func);
}
void Excute(int sock)
{
for(auto &f:func_)
{
//遍历vector中的所有方法
f(sock);
}
//给func_传参,把套接字传进去
}
void Start()
{
for(;;)
{
std::string clientip;
uint16_t clientport;
int sock=sock_.Accept(listensock_,&clientip,&clientport);
if(sock==-1) continue;
logMessage(NORMAL,"%s","create new link success,socl:%d",sock);
pthread_t tid;//创建线程
ThreadData *td=new ThreadData(sock,this);//创建一个线程对象
pthread_create(&tid,nullptr,ThreadRoutine,td);
//ThreadRoutine参数td是一个线程对象,该对象内存的是线程的数据
}
}
~TcpServer()
{
if(listensock_>=0)
close(listensock_);
}
private:
int listensock_;
Sock sock_;
std::vector<func_t> func_;
//让线程完成一堆的事情,使用vector容器
//std::unordered_map<std:;string,func_t> func_;//给每个方法绑定一个名字
};
}
makefile
.PHONY:all
all:client CalServer
client:CalClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -f client CalServer