一、服务器的初始化
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
1.创建套接字
socket():
⭐参数介绍:
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可。
class TcpServer
{
public:
TcpServer()
: _sockfd(defaultsockfd)
{
}
void Init()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
lg(Fatal, "create socket, errno : %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, socket: %d", _sockfd);
}
~TcpServer()
{
close(_sockfd);
}
private:
int _sockfd; // 套接字
};
2.绑定端口号和ip
bind():
⭐参数介绍:
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听 myaddr所描述的地址和端口号;
- 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们先来看看地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址 但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
- 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
- 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问 题;
我们的程序中对myaddr参数是这样初始化的:
- 1. 将整个结构体清零;
- 2. 设置地址类型为AF_INET;
- 3. 网络地址为0.0.0.0, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址;
class TcpServer
{
public:
TcpServer(const uint16_t& port, const string& ip = defaultip)
: _sockfd(defaultsockfd)
, _port(port)
, _ip(ip)
{
}
void Init()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, socket: %d", _sockfd);
//2.绑定端口号
// 使用这个结构体需要包头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 转化为网络序列
local.sin_port = htons(_port);
// 字符串转化为点时分形式的ip
inet_aton(_ip.c_str(), &(local.sin_addr));
//此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
}
~TcpServer()
{
close(_sockfd);
}
private:
int _sockfd; // 套接字
uint16_t _port; // 端口号
string _ip; // ip地址
};
3.设置监听状态
listen():
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5);
- listen()成功返回0,失败返回-1;
#pragma onec
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;
const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;
enum
{
UsageError = 1,
SocketError = 2,
BindError = 3,
ListenError = 4
};
class TcpServer
{
public:
TcpServer(const uint16_t& port, const string& ip = defaultip)
: _sockfd(defaultsockfd)
, _port(port)
, _ip(ip)
{
}
void Init()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, socket: %d", _sockfd);
//2.绑定端口号
// 使用这个结构体需要包头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 转化为网络序列
local.sin_port = htons(_port);
// 字符串转化为点时分形式的ip
inet_aton(_ip.c_str(), &(local.sin_addr));
//此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, socket: %d", _sockfd);
// 3.设置监听状态
// Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
if (listen(_sockfd, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "bind socket success, socket: %d", _sockfd);
}
~TcpServer()
{
close(_sockfd);
}
private:
int _sockfd; // 套接字
uint16_t _port; // 端口号
string _ip; // ip地址
};
4.设置服务器的端口号和ip
未来我们向让用户设置端口号,所以我们可以在main函数中借助命令行参数传递,对于ip我们就直接使用默认的ip参数"0.0.0.0"即可。
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
// ./tcpserver 8080
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(UsageError);
}
uint16_t port = std::stoi(argv[1]);
unique_ptr<TcpServer> tcpSvr(new TcpServer(port));
tcpSvr->Init();
//tcpSvr->Start();
return 0;
}
现在我们就可以来测试一下啦!
二、服务器的运行
1.建立新链接
accept():
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
- 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。
⭐accept函数返回的套接字是什么?
调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。
⭐监听套接字与accept函数返回的套接字的作用:
- 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
- accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,类似于餐厅门口的迎宾,而真正为这些连接提供服务的套接字是accept函数返回的套接字,类似于餐厅的服务员,而不是监听套接字。
所以初始化TCP服务器时创建的套接字应该叫做监听套接字。为了表明寓意,我们将代码中套接字的名字由_sockfd改为_listensocket,这样写着更清楚明了。
#pragma onec
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;
const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
Log lg;
enum
{
UsageError = 1,
SocketError = 2,
BindError = 3,
ListenError = 4
};
class TcpServer
{
public:
TcpServer(const uint16_t& port, const string& ip = defaultip)
: _listensocket(defaultsockfd)
, _port(port)
, _ip(ip)
{
}
void Init()
{
// 1.创建套接字
_listensocket = socket(AF_INET, SOCK_STREAM, 0);
if (_listensocket < 0) // 创建套接字失败
{
lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, socket: %d", _listensocket);
//2.绑定端口号
// 使用这个结构体需要包头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// 转化为网络序列
local.sin_port = htons(_port);
// 字符串转化为点时分形式的ip
inet_aton(_ip.c_str(), &(local.sin_addr));
//此时我们仅仅只在用户栈填好了,并没有写进到打开的网络文件和套接字中,没有设置系统中
int n = bind(_listensocket, (const struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, socket: %d", _listensocket);
// 3.设置监听状态
// Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
if (listen(_listensocket, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "bind socket success, socket: %d", _listensocket);
}
void Start()
{
lg(Info, "tcpServer is running....");
for (;;)
{
// 1. 获取新连接 - 知道客户端的ip地址和端口号
struct sockaddr_in client;
socklen_t len = sizeof(client);
// _sockfd的核心工作是: 从底层获取客户端的请求 - 餐厅门口的迎宾
// sockdf的核心工作是: 处理会去来的客户请求 - 餐厅的服务员
int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
// 从底层获取客户端的请求 - 餐厅门口的迎宾 - 路人不来吃饭 - 换一下批路人
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
continue; // 所以这里使用continue
}
lg(Info, "get a new link..., sockfd: %d", sockfd);
}
}
~TcpServer()
{
close(_listensocket);
}
private:
int _listensocket; // 套接字
uint16_t _port; // 端口号
string _ip; // ip地址
};
此时我们想来测试一下,但是我们的客户端还没有写,咋办呢?
telnet 127.0.0.1 8888
是一个网络诊断命令,它的作用是尝试通过Telnet协议连接到本地主机(本机)的8888端口。这里:
telnet
是命令本身,用于进行远程登录和管理。127.0.0.1
是IPv4环回地址,指向本地计算机。使用这个地址,你实际上是尝试连接到你自己的机器。8888
是端口号,许多应用程序和服务会监听特定的端口来接收数据。8888是一个常见的测试或备用端口。
上一个知识点我们提到udp它是不能绑定我们云服务器的公网ip的,但是绑定本地环回127.0.0.1可以的,我们看看tcp可不可以。