目录
前言
一、基于UDP协议套接字的编写
1、UDP协议特点
2、IP地址与端口号在网络通信中的作用
3、socket套接字接口介绍
4、网络字节序
5、基于UDP套接字使用的实战演练
二、基于TCP协议套接字的编写
1、TCP协议特点
2、socket套接字接口的补充介绍
3、UDP套接字编写 VS TCP套接字编写
5、基础TCP套接字使用的实战演练
本文就以 UDP / TCP 这两种网络协议来编写套接字代码;本文主要以套接字的应用为主,关于网络协议的原理我们将在后面的文章进行介绍;
首先,我们要清楚的是UDP协议是一种面向数据报,且无连接的网络协议;关于面向数据包,我们可以理解为UDP协议发送的报文是完整一块的,接收时,我们也是一块一块的接收的;所谓无连接,就是UDP进行网络通信时,并不需要建立连接;
在网络通信中,其本质实际上就是一台主机上的某个进程相与另一台主机上的某一个进程进行通信,也就是跨主机的进程间通信,因此,我们需要用IP来确定网络上的唯一一台主机,用端口号来确定该主机上的某一个进程,这样便可以达到远程通信的效果;
这里小伙伴可能有疑惑了,为什么用端口号,而不采用进程pid的方案呢?实际上,进程pid是进程管理提出的,而我们这里需要进行网络通信,为了将网络模块和进程模块解耦合,因此我们选择使用端口号,而不使用进程pid;
注意:一个端口号只能绑定一个进程,而一个进程可以被多个端口号绑定;
int socket(int domain, int type, int protocol);
这个函数会创建一个网络套接字,若成功创建则返回一个文件描述符,若失败,则返回-1;
参数
参数一:这个参数为我们使用创建套接字的协议,常见参数如下;
AF_INET(PF_INET):IPV4
AF_INET:IPV6
参数二:这个参数即我们创建套接字的类型,常见参数如下;
SOCK_DGRAM:无连接,面向数据包,常用于UDP
SOCK_STREAM:有连接,面向字节流,常用于TCP
参数三:协议,这个字段我们填0表示默认即可;
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
给当前进程绑定IP以及port(端口)等信息,若绑定成功则返回0,若失败返回-1;
参数
参数一:我们要为哪个套接字绑定,这里填我们socket的返回值即可;
在介绍参数二与参数三前,我们首先要了解如下结构;
通过前面16位我们不难看出我们稍后需要使用网络通信时,我们需要使用第二种结构,我们通过这种结构可以绑定端口和IP,也可以通过这种结构获取IP与端口;
参数二:我们发现参数二位结构以的类型,因此我们在填写该参数时,需要将参数二的类型进行强制类型转换即可;
参数三:该参数位参数二的大小;
ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
int flags, struct sockaddr *restrict address,
socklen_t *restrict address_len);该函数用于基于UDP网络通信时,数据的接收,返回值为接收数据的字节数,若返回值为0,则表示对端关闭,若为-1则表示调用失败,错误码被设置
参数
参数一:我们要哪个套接字获取数据
参数二:该参数为输出型参数,表示我们要将接收数据存到哪里,也就是接收缓冲区;
参数三:该参数为我们接收缓冲区的大小;
参数四:标志位的设置,这里我们填0即可;
参数五:该参数为输出型参数,接收发送放的IP与port;
参数六:该参数为输入输出型参数,输入参数五的大小,输出我们收到参数五的大小;
ssize_t sendto(int socket, const void *message, size_t length,
int flags, const struct sockaddr *dest_addr,
socklen_t dest_len);该函数为基于UDP协议网络通信的发送数据的接口,我们可以通过该接口发送我们要发送的数据,若调用成功返回我们成功发送数据的字节数,若调用失败,返回-1,错误码被设置;
参数:
参数一:该参数为我要往哪里套接字写入数据;
参数二:该参数为我们要发送数据的位置,也就是发送缓冲区;
参数三:这个参数为我们发送缓冲区大小;
参数四:这个参数为标志位,我们设置为0即可;
参数五:该参数为输入型参数,表示我们要将数据发送给哪一台主机,这个结构体填对端主机的IP和端口即可;
参数六:该参数为输入型参数,填入参数五的大小即可;
对于UDP协议,我们仅仅只需要使用上面几个套接字接口即可,在后面的TCP协议中,我们还会补充三个接口函数的使用;
当我们学习完上面接口后,我们还有一个问题没有解决,我们在进行网络传输时,我们可能需要考虑大小端等问题,因为我们在进行网络传输时,我们无法得知对端机器时大端还是小端,因此我们需要执行一个统一策略,因此诞生了我们的网络字节序,我们统一使用大端作为网络字节序;
这时,可能有些小伙伴就很恼火了,那我们目前可能常见的都为小端字节序,那我们不是还要将我们的小端字节序转成大端字节序,这也太麻烦了吧,因此,诞生了如下一批接口;
我们讲解其中一个函数,剩下来的大家自己理解即可;
uint16_t htons(uint16_t hostshort);
该函数将一个16位的整型由主机序列转成网络序列;其中h表示host主机,n表示net网络,s表示短整型;其他三个接口也可以这么理解;
我们发现我们在对IP地址进行转化时,也比较麻烦,我们知道,IP地址为4字节,其中我们采用点分十进制的表示方式来表示IP地址;实际数据传输时,我们通常转换成整型,再转换成网络序列;因此它们之间的相互转换也很麻烦,因此又诞生了如下接口;
其中,前三个函数时将我们的IP地址的网络序列转换成字符串的点分十进制;后面的那两个函数是将点分十进制的字符串转换成网络序列,其中可能很多人看到 in_addr_t 这个类型就蒙了,这里进行补充,实际上就是我们struct sockaddr_in中的一个成员;
其中第一个成员变量,看起来很复杂,调用了一个宏函数,进行拼接,如果看不懂可以直接理解成,这个参数填的是协议类型,也就是socket函数的第一个参数,我们通常填AF_INET;第二个成员变量为端口号,第三个参数我们看着很懵,转到定义查看,实际上就是一个结构体,而结构体就一个成员,其类型为in_addr_t,也就是我们上面第三个函数的返回值;该结构体内最后一个字段就是填充字段,我们可以不管;有了这个知识补充,接下来我们一个一个介绍上面函数;
int inet_pton(int af, const char *src, void *dst);
该函数将一个字符串点分十进制的IP地址转换成网络字节序的IP地址;若调用成功返回0,失败则返回-1,错误码被设置;
参数
参数一:协议类型(如AF_INET);
参数二:点分十进制的字符串;
参数三:输出型参数,我们将转换成的结果放入这个参数中;
int inet_aton(const char *cp, struct in_addr *inp);
该函数作用与上函数作用相同;
参数
参数一:该参数为我们要转换的IP字符串;
参数二:该参数为我们存放转换的结果,其中参数类型为in_addr*,这个参数就是struct sockaddr_in 中的一个字段,因此我们通常配合这个结构体一起使用;
in_addr_t inet_addr(const char *cp);
该函数功能同上,其中返回值为转化后的结果;
参数:参数只有一个,就是我们IP地址;
char *inet_ntoa(struct in_addr in);
该函数的功能为将一个网络序列的IP转换成点分十进制的字符串类型;其中返回值为字符串;
参数:该函数的参数只有一个,就是struct sockaddr_in 结构中的sin_addr字段,通常也会配合这个结构体一起使用;
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
该函数的功能同上,只不过用法可能存在不同;其中若函数调用成功则将dst,也就是转化后结果的地址,若失败,则返回空指针;
参数:
参数一:该参数为协议簇(如AF_INET等);
参数二:该参数为IP网络序列的地址,通常为strcut sockaddr_in 中 sin_addr的地址;
参数三:我们将转换好的结果放入dst这个指针指向的缓冲区中,通常我们需要提前申请好这个缓冲区大小;
参数三:参数三缓冲区的大小;
我们想实现一个客户端向服务器端发送一条信息,然后服务端将这个信息再发送回给客户端,客户端再先显示出来,我们向实现这样的一个简单的程序;不难想象,我们需要写两个源程序,一个是基于客户端,一个是基于服务端,其中,我们对服务端进行封装,客户端,我们就不进行封装了,此外我们还创建一个文件用于日志打印,一个文件记录错误码,因此我们需要如下文件;
首先,我们编写makefile文件,如下所示;
// makefile文件
.PHONY:all
all:udpserver udpclient
udpserver:UdpServer.cc
g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpserver udpclient
其中日志文件编写如下所示;
// Log.hpp文件
#pragma once
#include
#include
#include
#define Info 1
#define Debug 2
#define Warning 3
#define Error 4
#define Fatal 5
// 将错误等级转换成字符串
const char *getLevel(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
}
return "Unknow";
}
bool LogMessage(int level, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
char buf[1024];
int n = vsprintf(buf, fmt, args);
if (n < 0)
return false;
// 获取时间
time_t timestamp = time(nullptr);
struct tm *cur_time = localtime(×tamp);
// 打印输出
printf("[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);
return true;
}
其中错误码定义文件如下;
// Err.hpp文件
#pragma once
#define ARGS_ERR 1 // 参数有误
#define SOCK_ERR 2 // 创建套接字有误
#define BIND_ERR 3 // 绑定错误
上述三个文件没有任何难度,因此不做任何介绍,直接一笔带过;下面我们来编写服务器端的程序,首先作为一个服务器程序,我们需要有初始化和启动函数,如下所示;
// UdpServer.hpp文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
#include "Err.hpp"
namespace Udp
{
class UdpServer
{
public:
UdpServer(uint16_t port, const std::string &ip = "")
: _ip(ip), _port(port)
{
}
void init()
{
}
void start()
{
}
private:
int _sock;
std::string _ip;
uint16_t _port;
};
}
首先,我们来编写服务器初始化的代码;也就是init函数,对于一个基于UDP进行网络通信的服务器来说,我们需要创建一个套接字,并将该套接字绑定当前服务器程序进程的IP与端口号,对于UDP协议来说初始化的工作就是这么简单;
void init()
{
// 1、创建socket套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
LogMessage(Fatal, "create sock fail, errno:%d, errstr:%s\n", errno, strerror(errno));
exit(SOCK_ERR);
}
LogMessage(Info, "create sock success!");
// 2、绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 这里需要由点分十进制转4字节(云服务器无需指定某个固定的IP)
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
// 这里需要本机序列转网络序列
local.sin_port = htons(_port);
// 设置进内核数据结构中
int len = sizeof(local);
int n = bind(_sock, (struct sockaddr *)&local, len);
if (n == -1)
{
LogMessage(Fatal, "bind fail, errno:%d, strerr:%s\n", errno, strerror(errno));
exit(BIND_ERR);
}
LogMessage(Info, "bind success!");
}
接下来就是start的函数的编写,该函数主要负责启动我们的UDP服务器,然后不断的接收来自服务器的处理请求,因此我们的服务器通常是一个死循环,不会退出的;
void start()
{
char buf[1024];
// 服务器程序一般不会退出
while (true)
{
// 输出型参数
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
// 输入输出型参数
socklen_t len = sizeof(len);
// 通信
int n = recvfrom(_sock, buf, sizeof(buf), 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
LogMessage(Error, "recv fail, errno:%d, errstr:%s\n", errno, strerror(errno));
continue;
}
//LogMessage(Info, "reve success!");
buf[n] = 0;
// 业务处理
std::string ip = inet_ntoa(client.sin_addr);
uint16_t port = ntohs(client.sin_port);
printf("[%s-%d] %s\n", ip.c_str(), port, buf);
// 发送回给客户端
n = sendto(_sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&client, len);
if (n < 0)
{
LogMessage(Error, "send fail, errno:%d, errstr:%s\n", errno, strerror(errno));
}
//LogMessage(Info, "send success!");
}
}
这样,我们的UDP服务器服务端就编写好了,我们接着编写UdpServer.cc文件来调用这个服务器;
// UdpServer.cc文件
#include
#include
#include
#include "UdpServer.hpp"
static void Usage(const std::string& command)
{
std::cout << "Usage:\n" << command << " port\n";
}
int main(int argc, char* args[])
{
if(argc != 2)
{
Usage(args[0]);
exit(ARGS_ERR);
}
// 获取port
uint16_t port = atoi(args[1]);
std::unique_ptr pusr(new Udp::UdpServer(port));
// 初始化
pusr->init();
// 启动
pusr->start();
return 0;
}
接着我们来编写客户端的代码,客户端我们并没有进行封装,我们首先思考如下两个问题;
1、客户端是否需要进行绑定工作;答案当然是肯定的;
2、那么客户端是否需要我们自己绑定?
实际上不需要我们自己绑定,我们自己绑定也非常不合理,假如我们电脑上有两个程序,分别为程序A与程序B,若这两个程序在编写客户端代码时,并不知道对方客户端程序如何编写的,因此也不知道对方客户端端口号绑定的是哪一个,因此若它们恰巧绑定的是同一个端口号,此时不就乱套了?我们前面还规定了一个端口号只能被一个进程绑定,此时我们一个端口号被多个进程绑定,显然是不合理的!因此我们无需手动绑定,当我们发送数据时,会OS会自动帮我们完成绑定操作,此时的端口号便是随机分配的,不会产生冲突;
// UdpClient.cc文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
#include "Err.hpp"
static void Usage(const std::string &command)
{
std::cout << "Usage:\n"
<< command << " ip port\n";
}
int main(int argc, char *args[])
{
if (argc != 3)
{
Usage(args[1]);
exit(ARGS_ERR);
}
std::string ip = args[1];
uint16_t port = atoi(args[2]);
// 创建套接字
int sock = socket(PF_INET /*AF_INET*/, SOCK_DGRAM, 0);
if (sock < 0)
{
LogMessage(Fatal, "create sock fail, errno:%d, errstr:%s\n", errno, strerror(errno));
exit(SOCK_ERR);
}
std::string msg;
char buf[1024];
// 通信
while (true)
{
// 初始化要发送的对端信息
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_addr.s_addr = inet_addr(ip.c_str());
peer.sin_port = htons(port);
socklen_t len = sizeof(peer);
// 发送
std::cerr << "Enter Message# ";
getline(std::cin, msg);
if(msg == "quit")
{
LogMessage(Info, "see you latter!");
break;
}
int n = sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr *)&peer, len);
if (n < 0)
{
LogMessage(Error, "send fail, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
// 接收
struct sockaddr_in temp;
memset(&temp, 0, sizeof(temp));
len = sizeof(temp);
n = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&temp, &len);
if(n < 0)
{
LogMessage(Error, "recv fail, errno:%d, errstr:%s\n", errno, strerror(errno));
}
buf[n] = 0;
std::cout << "serve echo# " << buf << std::endl;
}
}
TCP协议是面向字节流,且无连接的一种协议,所谓面向字节流,即发送与接收是以流的形式呈现,可能这么说,还是不大理解,接下来,具体解释这个面向字节流;
我们前面的文章讲过,对于同层协议来说,彼此都认为是与对方直接通信的,就如同我们使用电话打电话给好友,我和我的好友都认为我是与他直接交流的,可实际上,我是将我要跟好友讲的话说给电话,然后电话将我所将的话转化成数字信号通过信号发送到好友电话中,然后再转化成音频信号发送给我的好友的;我们的TCP协议也是如此,TCP属于传输层协议,在传输层中,本机和对端主机都有一个发送缓冲区,当我们调用发送与接收系统调用时,实际上就是将拷贝到TCP的发送缓冲区,或是从接收缓冲区将数据拷贝到本地缓冲;
这里所谓的流,就是我们的数据并不像UDP数据报一样,是整块发送,整块接收,而是我们发送和接收都是以字节为单位,一次发送多字节,和一次接收多字节;就像水龙头一样,我们拧开多长时间,就流出多少水;
所谓连接,我们使用TCP协议时,会自动建立连接,也就是我们常说的三次握手,当我们关闭连接时,需要断开连接,也就是四次挥手,这个我们后面在进行讲解;
除了上面接口以外,在TCP协议簇中,我们还需要用到如下接口;
int listen(int socket, int backlog);
参数
参数一:socket创建时的文件描述符
参数二:全连接的连接数,这个可暂不了解,关于TCP的原理,后面会介绍;
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);该函数用于获取一个连接,若调用成功,返回一个sock文件描述符,若失败则返回-1;
参数
参数一:socket创建时返回的sock文件描述符;
参数二:该参数为输出型参数,返回对端的相关信息,如IP与Port等;
参数三:输入型参数,参数二的大小;
这里可能有小伙伴迷惑了,这里怎么传参传入一个sock文件描述符,返回又返回一个sock文件描述符,两者有什么联系呢?实际上,我们传入的第一个sock文件描述符是监听描述符,而真正提供服务的是第二个文件描述符;
这里举一个例子吧,我们平常可能都有去商场饭店吃饭的经历,通常门口会有一个人介绍他家的饭菜,此时如果你想去这家吃饭,就会被这个门口的服务员带进去,接着这个门口招待的服务员此时可能会再喊餐馆里来一个服务员带你进去找个位置坐下,并给你提供点餐服务,此时这个门口的服务员就会继续回到门口继续招揽客人;这里的门口服务员就像监听套接字,负责获取连接,而获取连接后(调用accept后),获取的那个新的sock套接字,就像真正给你提供服务的服务员;
ssize_t recv(int socket, void *buffer, size_t length, int flags);
ssize_t read(int fildes, void *buf, size_t nbyte);
这两个函数都可从网络中获取数据,也就是从接收缓冲区拷贝数据,后面的函数正是我们在文件操作中读取数据,我们主要介绍第一个函数,若成功读取到数据,则返回读取数据的字节数,若对端关闭,则返回0,若调用失败,则返回-1;
参数
参数一:accept获取后的套接字
参数二:输出型参数,该参数获取发送端信息;
参数三:参数二的大小;
参数四:标志位,设置为0即可;
注意:通常该接口只适用于TCP,也就是面向字节流的协议;
ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t write(int fildes, const void *buf, size_t nbyte);
这两个函数同样也都可以将数据拷贝到TCP协议的发送缓冲区,我们主要介绍其中第一个函数;
参数
参数一:accept获取后的套接字
参数二:发送缓冲区大小
参数三:发送数据的大小
参数四:标志位,通常设置为0
如下图所示,在我们使用这两种网络协议进行通信时,TCP的服务端需要设置监听状态,TCP的客户端需要发起连接,发起连接实际上就是进行TCP三次握手的过程;
同样,我们也想实现一个客户端发送一条信息,服务端马上就显示收到内容,然后将内容发送回指定客户端;同样,我们需要如下文件;
以下为makefile文件内容;
// Makefile文件
.PHONY:all
all:tcpserver tcpclient
tcpclient:TcpClient.cc
g++ -o $@ $^ -std=c++11
tcpserver:TcpServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpclient tcpserver
以下为错误码定义文件;
// Err.hpp文件
#pragma once
#define ARGS_ERR 1 // 缺少参数错误
#define SOCK_ERR 2 // 创建套接字失败
#define BIND_ERR 3 // 绑定失败
#define LISTEN_ERR 4 // 监听失败
#define ACCEPT_ERR 5 // 接受链接失败
#define CONNECT_ERR 6 // 连接失败
以下为日志打印文件代码;
// Log.hpp文件
#pragma once
#include
#include
#include
#define Info 1
#define Debug 2
#define Warning 3
#define Error 4
#define Fatal 5
const char *getLevel(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
}
return "Unknow";
}
bool LogMessage(int level, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
char buf[1024];
int n = vsprintf(buf, fmt, args);
if (n < 0)
return false;
// 获取时间
time_t timestamp = time(nullptr);
struct tm *cur_time = localtime(×tamp);
// 打印输出
printf("[%s][%d-%d-%d %d:%d:%d] %s\n", getLevel(level), 1900 + cur_time->tm_year, cur_time->tm_mon,\
cur_time->tm_mday, cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, buf);
return true;
}
这里同样,作为一个服务器程序必须要有初始化的启动,和上面UDP服务器程序一样,设计了init与start接口;具体代码如下;
// TcpServer.hpp文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Err.hpp"
#include "Log.hpp"
#include "ThreadPool/ThreadPool.hpp"
#include "ThreadPool/Task.hpp"
class TcpServer
{
static const int g_backlog = 32;
public:
TcpServer(uint16_t port, const std::string &ip = "")
: _ip(ip), _port(port)
{
}
void init()
{
// 创建套接字
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
LogMessage(Fatal, "create sock fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(SOCK_ERR);
}
LogMessage(Info, "create sock success, listenSock:%d", _listenSock);
// 绑定套接字
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
local.sin_port = htons(_port);
if (bind(_listenSock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
LogMessage(Fatal, "bind fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(BIND_ERR);
}
LogMessage(Info, "bind success");
// 监听
if (listen(_listenSock, g_backlog) < 0)
{
LogMessage(Fatal, "listen fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
LogMessage(Info, "listen success");
}
// v1:单执行流版,此时只能服务一个用户
void start()
{
while(true)
{
// 接收连接
struct sockaddr_in peer;
memset(&peer, 0 ,sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listenSock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
LogMessage(Error, "accept err, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
LogMessage(Info, "get a new link, sock:%d", sock);
// 进行服务
std::string ip = inet_ntoa(peer.sin_addr);
uint16_t port = ntohs(peer.sin_port);
Echo(sock, ip, port);
close(sock);
}
}
// 回显业务逻辑
void Echo(int sock, const std::string& ip, uint16_t port)
{
char buf[1024];
// 长连接服务
while(true)
{
// 接收远端发来信息 read/recv
int n = read(sock, buf, sizeof(buf) - 1);
if(n < 0)
{
// 读取失败
LogMessage(Error, "read fail, errno:%d, errstr:%s", errno, strerror(errno));
break;
}
else if(n == 0)
{
// 对端关闭
LogMessage(Info, "[%s-%d] quit, me too!", ip.c_str(), port);
break;
}
else
{
// 成功读取到数据,进行回显,并再次转发回去
buf[n] = 0;
std::cout << "server recv# " << buf << std::endl;
// 转发
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(ip.c_str());
client.sin_port = htons(port);
socklen_t len = sizeof(client);
// 发送这里可以用 write / send
int n = write(sock, buf, strlen(buf));
if(n < 0)
{
LogMessage(Error, "write fail");
continue;
}
}
}
}
private:
int _listenSock;
std::string _ip;
uint16_t _port;
};
以下为主函数调用代码;
// TcpServer.cc文件
#include
#include "TcpServer.hpp"
static void Usage(const std::string& command)
{
std::cout << "Usage:\n" << command < " serverport\n";
}
int main(int argc, char* args[])
{
if(argc != 2)
{
Usage(args[0]);
exit(ARGS_ERR);
}
uint16_t port = atoi(args[1]);
std::unique_ptr pts(new TcpServer(port));
pts->init();
pts->start();
return 0;
}
以下为客户端程序代码;
// TcpClient.cc文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Err.hpp"
#include "Log.hpp"
static void Usage(const std::string &command)
{
std::cout << "Usage:\n"
<< command <
" serverip serverport\n";
}
int main(int argc, char *args[])
{
if (argc != 3)
{
Usage(args[0]);
exit(ARGS_ERR);
}
std::string ip = args[1];
uint16_t port = atoi(args[2]);
std::cout << "sock front";
// 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
LogMessage(Fatal, "create sock fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(SOCK_ERR);
}
std::cout << "sock back";
LogMessage(Info, "create sock success, sock: %d", sock);
// 无需自己绑定,第一次发送时会自动绑定
// 连接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
socklen_t len = sizeof(server);
if(connect(sock, (struct sockaddr*)&server, len) < 0)
{
LogMessage(Fatal, "connect fail, errno:%d, errstr:%s", errno, strerror(errno));
exit(CONNECT_ERR);
}
// 通信
while (true)
{
// 发送 wriet/send
std::cout << "请输入# ";
std::string msg;
getline(std::cin, msg);
int n = send(sock, msg.c_str(), msg.size(), 0);
if (n < 0)
{
LogMessage(Error, "send fail, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
// 接收
char buf[1024];
n = recv(sock, buf, sizeof(buf) - 1, 0);
if (n < 0)
{
// 读取失败
LogMessage(Error, "recv fail, errno:%d, errstr:%s", errno, strerror(errno));
break;
}
else if (n == 0)
{
// 对端关闭
LogMessage(Info, "server quit, me too!");
break;
}
else
{
// 成功读取到数据,进行回显,并再次转发回去
buf[n] = 0;
std::cout << "server echo# " << buf << std::endl;
}
}
return 0;
}
上面程序编写完后我们是可以成功运行,但是仍然存在一个问题,在TcpServer.hpp文件中,当我们多个用户到来时,我们的服务器程序仍然只能处理一个用户的请求,只有当前用户退出后,才可以给另一个用户提供服务;
这是因为我们的Echo函数,也就是业务处理函数是一个死循环,因此只有这个死循环退出后,才可以继续获取连接,因此我们可以把上面的代码改成多进程方案,我们没收到一个连接,我们就创建一个子进程,为该连接提供业务服务,接着我们让父进程来继续获取连接,这里有一个小细节,我们父进程若不对子进程进行回收可能会产生僵尸进程的问题,而我们若对进程等待,阻塞等待方案就又回到了最初的问题,非阻塞方案则不知道我们应该有多少个进程需要回收,因此我们可以采用信号的方式,当自己成发送SIGCHLD信号时,我们直接让其自动释放,这样就可以解决僵尸进程的问题;代码如下(仅需替换下面两个函数模块即可);
// v2:多进程版本
void start()
{
// 处理子进程(方案一)
//signal(SIGCHLD, SIG_IGN); // 当子进程退出时,自动释放
while (true)
{
// 接收连接
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listenSock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
LogMessage(Error, "accept err, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
LogMessage(Info, "get a new link, sock:%d", sock);
// 进行服务
std::string ip = inet_ntoa(peer.sin_addr);
uint16_t port = ntohs(peer.sin_port);
// 让子进程去完成任务
// 这里需要考虑进程等待,因此有两种方案
// 方案一:使用信号,对于子进程发来的信号,我们使用默认动作
// pid_t id = fork();
// if(id == 0)
// {
// // 子进程
// close(_listenSock); // 关闭监听套接字
// Echo(sock, ip, port);
// exit(0);
// }
// // 父进程
// close(sock); // 关闭业务处理套接字
// 方案二:让孙子进程处理业务,而子进程直接退出,这时孙子进程就成为了孤儿进程
pid_t id = fork();
if(id == 0)
{
if(fork() > 0)
{
// 子进程直接退出
exit(0);
}
// 孙子进程处理业务
close(_listenSock);
Echo(sock, ip, port);
exit(0);
}
// 父进程
close(sock);
}
}
// 回显业务逻辑
void Echo(int sock, const std::string &ip, uint16_t port)
{
char buf[1024];
// 长连接服务
while (true)
{
// 接收远端发来信息 read/recv
int n = read(sock, buf, sizeof(buf) - 1);
if (n < 0)
{
// 读取失败
LogMessage(Error, "read fail, errno:%d, errstr:%s", errno, strerror(errno));
break;
}
else if (n == 0)
{
// 对端关闭
LogMessage(Info, "[%s-%d] quit, me too!", ip.c_str(), port);
break;
}
else
{
// 成功读取到数据,进行回显,并再次转发回去
buf[n] = 0;
//std::cout << "server recv# " << buf << std::endl;
printf("[%s-%d]server recv# %s\n", ip.c_str(), port, buf);
// 转发
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(ip.c_str());
client.sin_port = htons(port);
socklen_t len = sizeof(client);
// 发送这里可以用 write / send
int n = write(sock, buf, strlen(buf));
if (n < 0)
{
LogMessage(Error, "write fail");
continue;
}
}
}
}
上面我还提供了一种孙子进程方案,主要利用孤儿进程的特点来进行处理;
同样,我们还可以将上述代码改成多线程版本,没收到一个连接就创建一个子线程,让子线程去进行业务处理;
class ThreadDate
{
public:
int _sock;
std::string _ip;
uint16_t _port;
TcpServer* _ts;
};
// v3:多线程版本
void start()
{
while(true)
{
// 接收连接
struct sockaddr_in peer;
memset(&peer, 0 ,sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listenSock, (struct sockaddr*)&peer, &len);
if(sock < 0)
{
LogMessage(Error, "accept err, errno:%d, errstr:%s", errno, strerror(errno));
continue;
}
LogMessage(Info, "get a new link, sock:%d", sock);
// 进行服务
ThreadDate* td = new ThreadDate();
td->_sock = sock;
td->_ip = inet_ntoa(peer.sin_addr);
td->_port = ntohs(peer.sin_port);
td->_ts = this;
pthread_t servicer;
pthread_create(&servicer, nullptr, serviceRoutine, td);
}
}
// 提供服务
static void* serviceRoutine(void* args)
{
ThreadDate* td = static_cast(args);
pthread_detach(pthread_self());
std::cout << "sock:" << td->_sock << " ip:" << td->_ip << " port:" << td->_port << std::endl;
td->_ts->Echo(td->_sock, td->_ip, td->_port);
delete td;
return nullptr;
}
// 回显业务逻辑
void Echo(int sock, const std::string& ip, uint16_t port)
{
char buf[1024];
// 长连接服务
while(true)
{
// 接收远端发来信息 read/recv
int n = read(sock, buf, sizeof(buf) - 1);
if(n < 0)
{
// 读取失败
LogMessage(Error, "read fail, errno:%d, errstr:%s", errno, strerror(errno));
break;
}
else if(n == 0)
{
// 对端关闭
LogMessage(Info, "[%s-%d] quit, me too!", ip.c_str(), port);
break;
}
else
{
// 成功读取到数据,进行回显,并再次转发回去
buf[n] = 0;
//std::cout << "server recv# " << buf << std::endl;
printf("[%s-%d]server recv# %s\n", ip.c_str(), port, buf);
// 转发
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = inet_addr(ip.c_str());
client.sin_port = htons(port);
socklen_t len = sizeof(client);
// 发送这里可以用 write / send
int n = write(sock, buf, strlen(buf));
if(n < 0)
{
LogMessage(Error, "write fail");
continue;
}
}
}
}