网络编程套接字
目录
零、预备知识
1.理解源IP地址和目的IP地址
2.认识端口号
3.理解 "端口号" 和 "进程ID"
4.理解源端口号和目的端口号
5.两个常见传输层协议
认识TCP协议
认识UDP协议
6.网络字节序
一、socket编程接口
1.socket 常见API
2.sockaddr结构
sockaddr 结构
sockaddr_in 结构
in_addr结构
二、udp代码示例
Log.hpp
Makefile
UdpServer.hpp
UdpClient.cc
Main.cc
接口
recvfrom
依次解决的问题及一些注意点
1.ip地址格式问题
2.一个关于ip的问题
3.一个关于端口号的问题
4.关于服务器端口号和客户端端口号的问题
5.本地环回地址
udp代码(过程版)
1.创建套接字
2.绑定套接字
3.收发消息
收消息
发消息
查看 udp信息
小知识:popen函数
4.windows中的代码书写
5.多线程分离读写 以及终端模拟图形化界面实现收发分离
Log.hpp
Makefile
Terminal.hpp
UdpServer.hpp
UdpClient.cc
Main.cc
小知识:关于inet_ntoa
三、TCP代码示例
telnet
connect 接口
一些小问题
1-3版代码
Log.hpp
TcpServer.hpp
TcpClient.cc
Makefile
结果示例
单次服务示例
重连模块示例
守护进程
1.前台进程与后台进程
2.前后台进程的一些操作
a.fg
b.jobs
c.ctrl + z
d.bg
3.Linux进程间关系
4.守护进程化
setsid
守护进程结果示例
系统Daemon函数
四、tcp协议
tcp通信时是全双工的
零、预备知识
1.理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
但仅仅有ip地址并不能准确知道要将数据给到对方机器的哪个程序进行解析,因此我们还要引入端口号的概念
2.认识端口号
端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用.
(一个进程可以对应多个端口号,但是一个端口号只能对应一个进程,可以结合函数的概念理解)
3.理解 "端口号" 和 "进程ID"
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 理论上是可以将进程id用于识别目标进程,但是由于进程id变化频繁并且出于网络与操作系统的解耦合理念,网络上新提出一个端口号的概念
4.理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";
5.两个常见传输层协议
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
传输层协议
有连接
可靠传输
面向字节流
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
传输层协议
无连接
不可靠传输
面向数据报
注意,这里的可靠与不可靠都是中性词,只是用于指代该协议的一个性质
6.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
4.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
5.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
将0x1234abcd写入到以0x0000开始的内存中,则结果为
big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x34 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
为使网络程序具有可移植性,使同样的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. 然而, 各种网络协议的地址格式并不相同.
补充知识:
套接字编程的种类
1.域间套接字编程-----一个机器内通信,本地通信
2.原始套接字编程-----一般用于编写网络工具
3.网络套接字编程------用户间通信
为了保证网络接口统一,每一个接口的参数必须统一
网络套接字使用 域间套接字使用
但是 网络接口设计的是第一种类型。使用如下.,是一种典型的多态的特点
1.Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
2.IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
3.socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr结构
二、udp代码示例
Log.hpp
#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;
};// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }// va_end(s); //s = NULL
// return sum;
// }
Makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient
UdpServer.hpp
#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 "Log.hpp"// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;Log lg;enum{SOCKET_ERR=1,BIND_ERR
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false){}void Init(){// 1. 创建udp socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct sockaddr_in local;bzero(&local, sizeof(local));//清零,也可以用memset进行清零操作local.sin_family = AF_INET;local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 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_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct 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 = func(info);sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}~UdpServer(){if(sockfd_>0) close(sockfd_);}
private:int sockfd_; // 网路文件描述符std::string ip_; // 任意地址bind 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;
};
UdpClient.cc
#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>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct 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){cout << "socker error" << endl;return 1;}// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// std::cout << message << std::endl;// 1. 数据 2. 给谁发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;cout << buffer << endl;}}close(sockfd);return 0;
}
Main.cc
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>// "120.78.126.148" 点分十进制字符串风格的IP地址void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;// pid_t id = fork();// if(id == 0)// {// // ls -a -l -> "ls" "-a" "-l"// // exec*();// }return res;
}std::string ExcuteCommand(const std::string &cmd)
{// SafeCheck(cmd);FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run(ExcuteCommand);return 0;
}
接口
recvfrom
发的接口和收也很类似
依次解决的问题及一些注意点
1.ip地址格式问题
我们日常常用的ip地址是类似192.168.1.33这种string类型的,但是计算机中需要的是一个四字节的ip,因此我们需要作转化,思路类似上面的伪代码,不过因为使用频繁,系统已经给我们提供了对应接口了。 不过实际应用时,如果ip要被网络使用,我们还需要注意转化成网络序列。
因为这个in_addr是一个结构体,所以要这么赋值。
2.一个关于ip的问题
例如我们这台主机有两个ip,一个是192.168.1.148,一个是192.168.1.149,如果我们固定绑定了一个ip,那么只能接收到发往这个ip的数据,但是如果我们绑定0,不管你是发给148的还是149的,只要是发给我这台主机的,都会被接收,然后根据端口号向上传递
并且在云服务器上禁止直接绑定公网ip,如果是虚拟机,那么可以。
3.一个关于端口号的问题
我们的服务器绑定端口号的时候,需要注意,最好绑定1024以及以上的端口号,因为0-1023是系统内定的端口号,一般都有固定的应用层协议使用,但是有些是例外,例如mysql是3306,因此我们端口号绑定一般绑定大一些的的比较好。
(我们用端口号的时候,端口号要是没开放还需要我们去开放,这样客户端才能给服务器发消息,不过只需要开放服务器的端口号即可,我们只要开放了服务器的端口号,那么服务器是可以给客户端发消息的)
4.关于服务器端口号和客户端端口号的问题
服务器它的端口号是几,必须是确定的,因为将来用户是要连接访问我们的服务器的,所以必须要知道服务器的ip地址和端口号。如果让服务器端口号随机绑定,那么就极度不方便我们寻找并连接,会出现比较大的问题。
而对于客户端,它的端口号是多少其实并不是那么重要,他只是需要一个端口号来保证它在主机上的唯一性而已,因此是不需要我们主动绑定的,一般是由操作系统自由随机选择。
系统什么时候给我bind呢?是在首次发送数据的时候
5.本地环回地址
127.0.0.1,通常被称为本地环回地址(Loopback Address) ,只能用于本地进程间通信,它对应的网络信息贯穿网络协议栈,送到最底层,但是不往网络里发,我们这台机子会把对应的信息又拿上来。通常用来进行client 和server的测试。它和本地地址区别很小,但是如果我们输一个实际的地址,例如120.55.47.xxx这种的,它是要通过网络的。
它可以在任意服务器下进行绑定。
udp代码(过程版)
1.创建套接字
domain是域(协议家族),里面填的不同内容表示ipv4,ipv6等 ,比如我们经常填AF_INET
type是套接字类型
SOCK_STREAM是流式套接字,SOCK_STREAM是用户数据报套接字。就是要求这个套接字未来给我们什么样的服务,是面向字节流的还是面向用户数据报的
protocol代表的是协议类型
实际上有前面两个,协议的类型以及很清楚了,因此很多时候我们填0即可
对于返回值,如果成功,一个新的套接字被返回如果失败,-1被返回,错误码被设置。这说明我们socket的返回值是一个文件,创建一个套接字的本质,在底层就相当于打开一个文件,只不过这个struct file指向的是底层的网卡设备。但是我们在进行收发之前,都必须得有一个参数,这个参数就是socket的返回值。
(find文件名 可以查看路径)
2.绑定套接字
sockfd就是我们刚刚创建好的那个套接字
addr是一个结构体
addrlen是这个结构体的长度
bzero把指定类型置为0
一些工具函数
3.收发消息
收消息
就是从指定的套接字里面读取一个报文,同时我们需要知道是谁给我们发的消息,因此需要用输出型参数来保存相应的信息。 flag我们一般设为0,以阻塞方式
发消息
与revcfrom是非常相似的,不过这个方法中的dest_addr与 addrlen属于输入型参数
查看 udp信息
带了n指的是把能显示成数字的都显示成数字(和下面这个不带n的对比来看就很明显了)
a表示all
u表示udp信息
proto表示 用的协议
Recv 和Send 表示收发报文的个数
Local Address表示本地的地址
Foreign表示的是远端 ,凡是为0.0.0.0的表示可以接收来自任何客户端的消息
State是状态
aup
nau
不带p就是不显示PID的信息了
小知识:popen函数
这个函数会帮我们fork,会让父子进程建立管道,然后让子进程将它的运行结果通过管道返回给父进程。如果父进程想得到command的运行结果,可以通过文件指针的方式读取,至于后面的type参数,其实就相当于把这个命令当文件,是进行读还是写等等,我们输r就是读。用完了就pclose关掉
log.hpp
#pragma once#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.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 3class 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);//把字符串存到leftbuffer里面// va_list s;// va_start(s, format);// char rightbuffer[SIZE];// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了// //格式 默认部分(左)+自定义部分(右)// char logtxt[SIZE*2];// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);// printLog(level, logtxt);//暂时打印// }void printLog(int level, std::string logtxt){switch (printMethod){case Screen:std::cout << logtxt << std:: endl;break;case Onefile:printOneFile("LogFile" ,logtxt);break;case Classfile:printClassFile(level, logtxt);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);//LogFileif(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);//LogFile.Debug/Warning/FatalprintOneFile(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);//把字符串存到leftbuffer里面va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了//格式 默认部分(左)+自定义部分(右)char logtxt[SIZE*2];snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);}
private:int printMethod;std :: string path;
};
Makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient
UdpServer.hpp
#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"Log.hpp"//using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;extern Log log;
enum{SOCKET_ERR = 1,BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): sockfd_(0),port_(port),ip_(ip),isrunning_(false){}void Init(){//1.创建udp socketsockfd_ = socket(AF_INET,SOCK_DGRAM, 0);if(sockfd_ < 0){log(Fatal, "socket create error, sockfd: %d",sockfd_);exit(SOCKET_ERR);}log(Info,"socket create success, sockfd: %d", sockfd_);//2.bind socketstruct sockaddr_in local;//#include<netinet/in.h>bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);//因为我们还需要把端口号发给其它进程,因此填入的端口号应该是网络字节序的//local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string->uint32_t 2.uint32_t 必须是网络字节序的 这个方法能把字符串转四字节,并且还能转成网络字节序local.sin_addr.s_addr = INADDR_ANY;//这个就是全0 并且因为全0,所以也不需要转网络字节序if(bind(sockfd_,(const struct sockaddr*)&local, sizeof(local)) < 0){log(Fatal,"bind errno: %d, err string %s", errno, strerror(errno));exit(BIND_ERR);}log(Info,"bind success, errno: %d, err string: %s",errno, strerror(errno));}void Run(func_t func)//传入这个函数是为了让代码得以分层{isrunning_ = true;char inbuffer[size];while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_,inbuffer, sizeof(inbuffer) - 1, 0 , (struct sockaddr*)&client, &len);//这里看成字符串用,所以-1if(n < 0){log(Warning, "recvfrom errno: %d, err string %s", errno, strerror(errno));continue;}inbuffer[n] = 0;std::string info = inbuffer;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:int sockfd_; //网络文件描述符std::string ip_; uint16_t port_;//表示服务器进程的端口号bool isrunning_;
};
UdpClient.cc
#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>using namespace std;void Usage(std:: string proc)
{std::cout <<"\n\r Usage: " << proc << " serverip serverport "<< std::endl;
}
//./udpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3)//可执行程序名是一个 ip地址是一个 端口号是一个{Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct 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){cout <<"socker error" << endl;return 1;}//client 要bind吗? 要 只不过不需要用户显式地区bind 一般由操作系统自由随机选择//一个端口号只能被一个进程bind 对server如此,对client也是如此//其实client的port是多少,并不重要 只要能保证它在主机上的唯一性即可//系统什么时候帮我们bind呢? 首次发送数据的时候string message;char buffer[1024];while(true){cout << "Please Enter@";getline(cin, message);//1.数据 2.给谁发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);//这里1023属于硬编码,写了个固定值 因为我们上面已经把buffer的大小硬性规定了if(s > 0)//recvfrom返回接收到的字节数{buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
Main.cc
#include "UdpServer.hpp"
#include "Log.hpp"
#include <memory>
#include<cstdio>
#include<vector>Log log;void Usage(std:: string proc)
{std::cout <<"\n\r Usage: " << proc << " port[1024+]\n" << std::endl;
}
std::string Handler(const std::string & str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}
bool SafeCheck(const std:: string &cmd)
{int safe = false;std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(auto & word: key_word){auto pos = cmd.find(word);if(pos != std::string::npos)return false;}return true;
}
std::string ExcuteCommand(const std::string &cmd)
{if(!SafeCheck(cmd)) return "Bad man";FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return"error";}std::string res;char buffer[4096];while(true){char *jude = fgets(buffer, sizeof(buffer), fp);//fgets 按行读if(jude == nullptr) break;res += buffer;}pclose(fp);return res;
}
int main(int argc, char *argv[])
{if(argc != 2)//可执行程序名是一个 端口号是一个{Usage(argv[0]);exit(0);}uint16_t port = std:: stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(Handler);return 0;
}
4.windows中的代码书写
不同操作系统的网络部分协议都是相同的,因此编写的代码总体差异不会太大。windows里面写upd代码我们需要先套这么一个壳子,如下
#include<iostream>
#include<WinSock2.h>#pragma comment(lib, "ws2_32.lib")int main()
{std::cout << "hello client" << std::endl;WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);WSACleanup();return 0;
}
完整代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<WinSock2.h>
#include<Windows.h>//这里头文件包含要有顺序,先WinSock2.h再Windows.h
#include<cstdlib>#pragma warning(disable:4996)//把4996的warning给禁掉#pragma comment(lib, "ws2_32.lib")
uint16_t serverport = 12345;
std::string serverip = "120.55.47.126";//因为命令行不好用,所以直接把端口号和ip地址写死了int main()
{std::cout << "hello client" << std::endl;WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);struct 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());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cout << "socker error" << std::endl;return 1;}//client 要bind吗? 要 只不过不需要用户显式地区bind 一般由操作系统自由随机选择//一个端口号只能被一个进程bind 对server如此,对client也是如此//其实client的port是多少,并不重要 只要能保证它在主机上的唯一性即可//系统什么时候帮我们bind呢? 首次发送数据的时候std::string message;char buffer[1024];while (true){std::cout << "Please Enter@";getline(std::cin, message);//1.数据 2.给谁发sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);//这里1023属于硬编码,写了个固定值 因为我们上面已经把buffer的大小硬性规定了if (s > 0)//recvfrom返回接收到的字节数{buffer[s] = 0;std::cout << buffer << std::endl;}}closesocket(sockfd);WSACleanup();return 0;
}
5.多线程分离读写 以及终端模拟图形化界面实现收发分离
多线程分离读写,只需要我们再创建两个线程,把读和写的代码分别放进去就可以了 。
我们现在还没有能力实现图形化界面,但是我们可以使用多个终端来模拟
或者我们也可以手动进行重定向
Log.hpp
#pragma once#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.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 3class 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);//把字符串存到leftbuffer里面// va_list s;// va_start(s, format);// char rightbuffer[SIZE];// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了// //格式 默认部分(左)+自定义部分(右)// char logtxt[SIZE*2];// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);// printLog(level, logtxt);//暂时打印// }void printLog(int level, std::string logtxt){switch (printMethod){case Screen:std::cout << logtxt << std:: endl;break;case Onefile:printOneFile("LogFile" ,logtxt);break;case Classfile:printClassFile(level, logtxt);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);//LogFileif(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);//LogFile.Debug/Warning/FatalprintOneFile(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);//把字符串存到leftbuffer里面va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了//格式 默认部分(左)+自定义部分(右)char logtxt[SIZE*2];snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);}
private:int printMethod;std :: string path;
};
Makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient
Terminal.hpp
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>std::string terminal = "/dev/pts/2";int OpenTerminal()
{int fd = open(terminal.c_str(), O_WRONLY);if(fd < 0){std::cerr << "open terminal error" << std::endl;return 1;}dup2(fd, 2);//往标准错误里面打return 0;
}
UdpServer.hpp
#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"Log.hpp"
#include<unordered_map>//using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string & , const std::string &,uint16_t)> func_t;extern Log log;
enum{SOCKET_ERR = 1,BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): sockfd_(0),port_(port),ip_(ip),isrunning_(false){}void Init(){//1.创建udp socket//2.Udp 的socket是全双工的,允许同时读写的。sockfd_ = socket(AF_INET,SOCK_DGRAM, 0);if(sockfd_ < 0){log(Fatal, "socket create error, sockfd: %d",sockfd_);exit(SOCKET_ERR);}log(Info,"socket create success, sockfd: %d", sockfd_);//2.bind socketstruct sockaddr_in local;//#include<netinet/in.h>bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);//因为我们还需要把端口号发给其它进程,因此填入的端口号应该是网络字节序的//local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string->uint32_t 2.uint32_t 必须是网络字节序的 这个方法能把字符串转四字节,并且还能转成网络字节序local.sin_addr.s_addr = INADDR_ANY;//这个就是全0 并且因为全0,所以也不需要转网络字节序if(bind(sockfd_,(const struct sockaddr*)&local, sizeof(local)) < 0){log(Fatal,"bind errno: %d, err string %s", errno, strerror(errno));exit(BIND_ERR);}log(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_){memset(inbuffer, 0, sizeof(inbuffer));struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_,inbuffer, sizeof(inbuffer) - 1, 0 , (struct sockaddr*)&client, &len);//这里看成字符串用,所以-1if(n < 0){log(Warning, "recvfrom 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);//inbuffer[n] = 0;//std::string info = inbuffer;//std::string echo_string = func(info, clientip, clientport);//收到信息时我们是从网络中收到的,所以它本身就是网络字节序了//sendto(sockfd_, echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client, len);}}~UdpServer(){if(sockfd_ > 0) close(sockfd_);//不过这里其实关不关都无所谓,因为当我们析构的时候这个服务器也已经关了,这个进程也已经结束了。而文件本身的生命是随进程的}
private:int sockfd_; //网络文件描述符std::string ip_; uint16_t port_;//表示服务器进程的端口号bool isrunning_;std::unordered_map<std::string, struct sockaddr_in> online_user;
};
UdpClient.cc
#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>
#include<string.h>
#include"Terminal.hpp"
using namespace std;void Usage(std:: string proc)
{std::cout <<"\n\r Usage: " << proc << " serverip serverport "<< std::endl;
}
//udp的socket是全双工的,可以同时读写 因此我们将客户端的代码多线程化。否则每次只有输入消息的时候才能读到一次消息,其他时候会被阻塞住
//./udpclient serverip serverportstruct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};
void *recv_message(void *args)
{OpenTerminal();ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while(true){struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0,(struct sockaddr*)&temp, &len);//这里1023属于硬编码,写了个固定值 因为我们上面已经把buffer的大小硬性规定了if(s > 0)//recvfrom返回接收到的字节数{buffer[s] = 0;cerr << buffer << endl;//由于我们Terminal中把文件重定向了,因此这里我们用cerr}}
}
void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);string message;socklen_t len = sizeof(td->server);std::string welcome = td->serverip;welcome += "comming....";sendto(td->sockfd, welcome.c_str(),welcome.size(), 0,(struct sockaddr *)&(td->server), len);//新用户加入的时候进行一个播报while(true){cout << "Please Enter@";getline(cin, message);//1.数据 2.给谁发sendto(td->sockfd, message.c_str(),message.size(), 0,(struct sockaddr *)&(td->server), len);}}
int main(int argc, char *argv[])
{if(argc != 3)//可执行程序名是一个 ip地址是一个 端口号是一个{Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct ThreadData td;bzero(&td.server, sizeof(td.server));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){cout <<"socker error" << endl;return 1;}td.serverip = serverip;pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender,nullptr,send_message,&td);//client 要bind吗? 要 只不过不需要用户显式地区bind 一般由操作系统自由随机选择//一个端口号只能被一个进程bind 对server如此,对client也是如此//其实client的port是多少,并不重要 只要能保证它在主机上的唯一性即可//系统什么时候帮我们bind呢? 首次发送数据的时候pthread_join(recvr,nullptr);pthread_join(sender,nullptr);close(td.sockfd);return 0;
}
Main.cc
#include "UdpServer.hpp"
#include "Log.hpp"
#include <memory>
#include<cstdio>
#include<vector>Log log;void Usage(std:: string proc)
{std::cout <<"\n\r Usage: " << proc << " port[1024+]\n" << std::endl;
}
std::string Handler(const std::string & info, const std::string & clientip,uint16_t clientport)
{std::cout << "[" << clientip <<":" << clientport << "]#" << info << std::endl;std::string res = "Server get a message: ";res += info;std::cout << res << std::endl;return res;
}
bool SafeCheck(const std:: string &cmd)
{int safe = false;std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top","while"};for(auto & word: key_word){auto pos = cmd.find(word);if(pos != std::string::npos)return false;}return true;
}
std::string ExcuteCommand(const std::string &cmd)
{if(!SafeCheck(cmd)) return "Bad man";FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return"error";}std::string res;char buffer[4096];while(true){char *jude = fgets(buffer, sizeof(buffer), fp);//fgets 按行读if(jude == nullptr) break;res += buffer;}pclose(fp);return res;
}
int main(int argc, char *argv[])
{if(argc != 2)//可执行程序名是一个 端口号是一个{Usage(argv[0]);exit(0);}uint16_t port = std:: stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run();return 0;
}
小知识:关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
三、TCP代码示例
tcp代码与udp代码大致上是一样的。基本思路类似 。
(在云服务器上,tcp与udp一样,都无法直接绑定公网ip,如下)
而本地环回是可以的
第一点不同在套接字的创建上。
udp的参数需要的是SOCK_DGRAM
sockfd_ = socket(AF_INET,SOCK_DGRAM, 0);
tcp的参数需要的是SOCK_STREAM
listensock = socket(AF_INET, SOCK_STREAM, 0);
第二点是由于 tcp是面向连接的,因此服务器是比较被动的。 服务器一直处于一种,一直在等待连接到来的状态
因此服务器需要进行监听,即listen
listen(listensock,backlog)
并且需要获取新连接
struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock, (struct sockaddr *)&client, &len);
telnet
telnet可以进行指定服务的远程登录,它在底层默认使用的就是tcp。
可以如下进行操作。按ctrl + ] 进入,退出输入quit即可
这里我们提一个小点
我们ip地址和端口号在网络传输时都要进行网络序列和主机序列的转化,但是我们发过去的消息不做大小端的转化吗?我们read,write进行读写时不做大小端的转化吗?实际上,套接字里面正常的通信内容,我们所使用的接口会默认给我们进行大小端的转化,会自动做。而ip地址和端口号比较特殊一点,是要写入操作系统的,因此需要我们手动来进行操作。
connect 接口
它的后两个参数和sendto一样,在connect接口内意思就是向谁发起连接。
还值得注意的是,connect的返回值,这里说明了,连接或绑定成功的时候,0被返回,因此客户端的随机绑定是在发起连接的时候进行的。
例如我们游戏掉线了,本质上可能是我们的连接出现问题了,进行断线重连就是客户端在进行重新connect。
一些小问题
1.如果我的客户端退出而服务器仍然运行,会怎么样呢?
答,服务器端read时,会读到0。
2.当我们用多个客户端访问时候,为什么只有一个客户端是正常通信的,在关闭这个客户端之后,消息全部跳出
因为目前我们写的是一个单进程的服务 。只有结束上一个客户端进程,Service函数退出后,才会继续进行for循环提供服务。所以我们再写多进程版本的代码。
3.一个需要注意的声明
因为ThreadData里面要用TcpServer而且 TcpServe的类在后面,所以我们需要在这里添一个TcpSever
4.write是有返回值的,成功的话就返回写入的字节数,失败则返回-1并设置错误码。
5. 我们知道,在管道中,我们的读端如果被关闭了,而写端还在写,那么当前进程就会收到一个sigpipe的信号,操作系统就会把这个进程干掉。这种情况在网络中也可能存在,会存在写崩溃的问题。所以我们经常会对sigpipe信号进行sigign
signal(SIGPIPE,SIG_IGN);
6.一般的服务通常是只给我们服务一次, 我们写的代码,如下,是用了while循环的。因此会导致客户不退的情况下 我们使用多线程时线程越来越多的情况。一般的服务都是服务一遍,即没有while循环。我们在之后的线程池版本代码进行修改
void Service (int sockfd, const std::string &clientip, const uint16_t &clientport){//测试代码char buffer[4096];while(true){size_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, clientip: %s, client port: %d",sockfd, clientip.c_str(), clientport);break;}}}
1-3版代码
Log.hpp
#pragma once#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.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 3class 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);//把字符串存到leftbuffer里面// va_list s;// va_start(s, format);// char rightbuffer[SIZE];// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了// //格式 默认部分(左)+自定义部分(右)// char logtxt[SIZE*2];// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);// printLog(level, logtxt);//暂时打印// }void printLog(int level, std::string logtxt){switch (printMethod){case Screen:std::cout << logtxt << std:: endl;break;case Onefile:printOneFile("LogFile" ,logtxt);break;case Classfile:printClassFile(level, logtxt);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);//LogFileif(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);//LogFile.Debug/Warning/FatalprintOneFile(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);//把字符串存到leftbuffer里面va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了//格式 默认部分(左)+自定义部分(右)char logtxt[SIZE*2];snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);}
private:int printMethod;std :: string path;
};
TcpServer.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string>
#include<pthread.h>
#include"Log.hpp"const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
Log lg;
const int backlog = 10; //这里我们暂时不作介绍,但是这个值一般不要设置得太大enum{UsageError = 1,SocketError,BindError,ListenError
};
class TcpServer;//因为ThreadData里面要用TcpServer而且 TcpServe的类在后面,所以我们需要在这里添一个TcpSever
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;std::string clientip;uint16_t clientport;TcpServer *tsvr;
};
class TcpServer
{
public:TcpServer(const uint16_t &port, const std::string &ip = defaultip):listensock(defaultfd), port_(port),ip_(ip){}void InitServer(){listensock = socket(AF_INET, SOCK_STREAM, 0);if(listensock < 0){lg(Fatal,"create socket errno: %d, errstring %s",errno, strerror(errno));exit(SocketError);}lg(Info,"create socket success, listensock: %d", listensock);struct 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));if(bind(listensock, (struct sockaddr*)&local, sizeof(local))< 0){lg(Fatal, "create socket errno: %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info,"bind socket success, listensock: %d", listensock);//tcp是面向连接的,因此服务器是比较被动的。 服务器一直处于一种,一直在等待连接到来的状态if(listen(listensock,backlog) < 0){lg(Fatal, "listen errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info,"listen socket success, listensock: %d", listensock);}static void *Routine(void * args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->tsvr->Service(td->sockfd, td->clientip, td->clientport);delete td;return nullptr;}void Start(){lg(Info, "tcpServer is running...");for(; ;){//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, error: %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\n", sockfd);//version1 单进程版 只能与一个客户端通信//Service(sockfd, clientip,clientport);//close(sockfd);//Service函数里面,当跳出while循环后Service结束,对应的sockfd也就没用了//version 2 多进程版 成本太大,资源消耗多//pid_t id = fork();//if(id == 0)//{//child//子进程是可以看到父进程的文件描述符的 //不用的文件描述符可以暂时关掉// close(listensock);// if(fork() > 0)exit(0);//直接退出子进程,把Service交给孙子进程// Service(sockfd, clientip, clientport);//孙子进程在其父进程退出后,被系统领养,它退出后不需要我们进行等待// close(sockfd);// exit(0);//}//father//close(sockfd);//这里把父进程的文件描述符关了,因为sockfd已经被子进程拿到,如果不关,那么文件描述符会越用越少//pid_t rid = waitpid(id, nullptr, 0);//这里进程等待我们也可以放到信号里面做,这里不过多介绍//(void)rid;//version 3多线程//线程中的绝大部分资源都是共享的 所以不需要我们关文件描述符,因为都是需要的ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);}}void Service (int sockfd, const std::string &clientip, const uint16_t &clientport){//测试代码char buffer[4096];while(true){size_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, clientip: %s, client port: %d",sockfd, clientip.c_str(), clientport);break;}}}~TcpServer(){}private:int listensock;//负责获取新连接uint16_t port_;std::string ip_;
};
TcpClient.cc
#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(std:: string proc)
{std::cout <<"\n\r Usage: " << proc << " serverip serverport "<< std::endl;
}
int main(int argc, char *argv[])
{if(argc != 3)//可执行程序名是一个 ip地址是一个 端口号是一个{Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET,SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}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));//tcp客户端要不要bind?要不要显式地bind?//和udp一样,需要绑定,但是不需要显式地绑定//客户端发起connect的时候,进行自动随机bindint n = connect(sockfd,(struct sockaddr*)&server, sizeof(server));if(n < 0){std::cerr << "connect error..." << std::endl;return 2;}std::string message;while(true){std::cout << "Please Enter# ";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());char inbuffer[4096];int n = read(sockfd, inbuffer, sizeof(inbuffer));if(n > 0){inbuffer[0] = 0;std::cout << inbuffer << std::endl;}}close(sockfd);return 0;
}
Makefile
.PHONY:all
all:tcpserver tcpclienttcpserver:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f tcpserver tcpclient
Main.cc
#include"TcpServer.hpp"
#include<iostream>
#include<memory>void Usage(std:: string proc)
{std::cout <<"\n\r Usage: " << 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]);std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));tcp_svr->InitServer();tcp_svr->Start();return 0;
}
结果示例
单次服务示例
重连模块示例
TcpClient.cc
#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(std::string proc)
{std::cout << "\n\r Usage: " << proc << " serverip serverport " << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3) // 可执行程序名是一个 ip地址是一个 端口号是一个{Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);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 sockfd = 0;int cnt = 5;int isreconnect = false; // 用于模拟重连do{sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}// tcp客户端要不要bind?要不要显式地bind?// 和udp一样,需要绑定,但是不需要显式地绑定// 客户端发起connect的时候,进行自动随机bindint 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;}std::string message;std::cout << "Please Enter# ";std::getline(std::cin, message);sleep(10); // 我们输入之后不立即写,先等10秒,这10秒内我们把服务器关掉,那么write就会出错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;
}
守护进程
1.前台进程与后台进程
Linux系统里,当我们进行登录时,Linux系统会给我们形成一个会话(session),并且在每个会话里,默认会有一个(且仅有一个)bash进程,此时这个bash进程就可以给用户提供命令行服务。这个bash进程和我们的键盘以及显示器是直接相关的。
如下,我们起的就是一个前台进程,此时我们输pwd,ls等指令,都起不了效果。
我们关闭这个进程,然后再起一个后台进程,我们发现输入pwd,ls,这些命令又能执行了。
我们还可以一次起多个后台进程。我们发现,此时我们输入ctrl + c是无法终止后台进程的。
其实一般而言一个session里面只能存在一个前台进程,但是允许存在很多的后台进程。我们在登录的时候,创建的bash就是一个前台进程。
当我们./创建一个进程的时候,操作系统会自动把bash切到后台。我们启动的这个进程就会变成前台进程
而当我们退出的时候,操作系统又会自己把bash切到前台来。
前台和后台进程都可以向显示器打印,而谁拥有键盘文件谁就是前台进程
2.前后台进程的一些操作
a.fg
front ground
fg + 后台任务号 把后台进程切成前台进程
我们起一个后台进程时会生成一个 本次任务号(也即后台任务号),即这里方括号里面的1.
把后台进程切成前台之后,就能用ctrl + c退出了
b.jobs
查看后台进程
c.ctrl + z
将前台进程切暂停并切到后台。
同样的,我们把后台进程切成前台之后,如果按的是ctrl + z(19号的暂停信号),那么操作系统也会自动把这个进程切回后台去,把bash切到前台。因为进程暂停后也接收不到我们的键盘输入了,所以这个进程不应该再占着前台进程了。
d.bg
bg + 后台进程号 启动后台暂停的进程
3.Linux进程间关系
预备工作:
这里的sleep 1000 | sleep 2000 | sleep 3000 没有什么实质的意思,只不过它会创建三个进程。
我们起了两组后台任务。
PGID即进程组ID, SID即session id,TTY标明的是属于哪个终端。
(我们登录时创建session,退出时释放session,所以理所当然,我们也要管理session)
我们注意到 ./process创建的这个进程,它的PID 与PGID是一样的,它的进程id与进程组id相同,那么说明它自己是自成一个进程组的。
而sleep相关的三个进程,他们的进程id不同,进程组id却都是相同的,都是第一个被创建的进程的pid。这个进程被称为组长,组长是多个进程中的第一个,它的pid同时被充当为组id。
提出问题,进程组和任务有什么关系呢?
(狭义上讲,我们之前的进程就叫做任务,进程控制块被称之为task struct。)
我们的任务是指派给进程组的。
这个任务可以由一个进程执行,即自己自成一个进程组。
也可以由多个进程共同执行。
所以上面的任务1任务2,最终会托付给相应的进程组执行。这里的任务是一个偏用户的概念。
到此,我们纠正一下我们的叫法。
前台进程后台进程,严格来说被称为前台任务和后台任务更为合适。
这些任务都在同一个session中,我们再顺便查看一下这个1351 的session号
发现了这个1351就是bash的pid。
所以,在我们登录时,操作系统分配bash的时候,会将bash的pid作为sessionid,构建一个session。我们后面所有的进程组都会放在这一个session中。
4.守护进程化
我们在某个会话创建一些进程,然后关掉这个会话。重新启动会话后我们再查看。我们发现这些进程并没有被终止,并且还是在同一会话中,但是很明显这些会话的PPID变成了1,它们都被系统领养了,说明这些进程还是受到了用户登录和退出的影响的。
我们如果不想让进程受到任何用户的登录和注销的影响,那么我们就需要守护进程化。
守护进程其实很简单,只需要把这个进程拎出来,让它自成会话,那么它就不受用户登录和注销的影响了。严格意义上来讲,它也属于后台进程的一种。只不过它和键盘没有关系。
setsid
setsid的功能就是创建一个会话,然后把该进程组的组id设成会话的id。
创建成功会把会话id返回,否则返回-1,同时错误码被设置。
但是这个函数接口有一个限制,即调用的进程不能是进程组的组长。
可是很多进程本身就自成一个进程组,那么我们该如何让 进程不是进程组的组长呢。我们可以fork,然后将父进程退出。 那么实际上守护进程本质上也是一个孤儿进程了。
守护进程结果示例
netstat -nltp 查看tcp服务
ps ajx | head -1 && ps ajx | grep tcpserver 查看tcp服务相关进程
我们可以看到,./tcpserver启动进程后,我们仍然可以使用ls指令。同时我们看到我们./tcpserver启动的进程其PPID是1,PID PGID SID都是19352,并且TTY为?,可以看到我们的守护进程化成功了。
由于我们并未更改 文件的保存路径,因此我们看到,这个进程的cwd并没有受到影响,还是在原路径
我们也可以查看此进程的文件描述符
我们创建的套接字是内存级文件,因此它是一闪一闪的
我们将打开进程的那个终端关闭,然后再启动tcpclient,我们发现服务并没有受到影响。
系统Daemon函数
第一个参数是是否将进程的工作目录改变到 /
第二个参数是是否把标准输入标准输出标准错误重定向到 /dev/null
四、tcp协议
tcp会三次握手来进行链接的建立。
通过四次挥手来进行链接的释放。
tcp的三次握手实际上可以算是tcp可靠性的一种 ,通信之前先通过三次握手把双方的链接维护好
tcp通信时是全双工的
tcp通信时是有自己的发送缓冲区和接收缓冲区的,实际上read和write可以看作拷贝函数,把用户区的数据拷贝到发送缓冲区或者把接收缓冲区的数据(内核数据)拷贝到用户区。
而缓冲区的数据什么时候发,发多少,出错了怎么办,完全由tcp自主决定。
我们的服务器是可以同时和多个客户端建立连接的,因此我们也需要管理连接,在三次握手结束后,客户端和服务器都需要各自创建连接结构体,这里的发送缓冲区和接收缓冲区都是在连接结构体中的。