作者:@小萌新
专栏:@网络
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:简单介绍网络的基础概念
我们使用一个类来封装服务端 当我们定义一个服务器对象之后马上就进行初始化 初始化TCP服务器第一时间就要创建套接字
我们在使用TCP服务的时候用socket函数创建套接字 参数设置如下
int socket(int domain, int type, int protocol);
AF_INET
因为我们要进行的是网络通信SOCK_STREAM
因为我们编写的是TCP服务器 SOCK_STREAM
提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务如果创建套接字后获得的文件描述符是小于0的 说明套接字创建失败 此时也就没必要进行后续操作了 直接终止程序即可
class TcpSever
{
private:
int _sockfd;
public:
void Init()
{
_sockfd = socket(AF_INET, SOCK_STREAM , 0);
if (_sockfd < 0)
{
cout << "socket error" << endl;
exit(2);
}
}
~TcpSever()
{
if (_sockfd > 0)
{
close(_sockfd);
}
}
};
这里需要注意的是:
套接字创建完毕之后我们实际上只是在系统层面上打开了一个文件 该文件还没有和网络关联起来 因此我们创建之后还需要使用bind函数绑定
绑定步骤如下
由于TCP服务器初始化时需要服务器的端口号,因此在服务器类当中需要引入端口号,当实例化服务器对象时就需要给传入一个端口号。而由于我当前使用的是云服务器,因此在绑定TCP服务器的IP地址时不需要绑定公网IP地址,直接绑定INADDR_ANY即可,因此我这里没有在服务器类当中引入IP地址。
class TcpSever
{
private:
int _sockfd;
int _port;
public:
void Init()
{
_sockfd = socket(AF_INET, SOCK_STREAM , 0);
if (_sockfd < 0)
{
cout << "socket error" << endl;
exit(2);
}
struct sockaddr_in local;
memset(&local , 0 , sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_sockfd , (struct sockaddr*)&local , sizeof(sockaddr)) < 0)
{
cout << "bind error" << endl;
exit(3);
}
}
public:
TcpSever(int port)
:_sockfd(-1),
_port(port)
{}
~TcpSever()
{
if (_sockfd > 0)
{
close(_sockfd);
}
}
};
在这之后我们的服务端绑定便完成了 此时我们会发现TCP和UDP的创建套接字和绑定步骤没有任何的区别
我们真正有区别的是下一步 服务器监听
因为TCP服务器是面向连接的 客户端在正式向TCP服务器发送数据之前需要建立连接
因此TCP服务器需要随时注意是否有客户端的连接请求 此时我们需要将状态设置为监听状态
listen函数
设置套接字为监听状态的函数叫做listen 该函数的函数原型如下:
int listen(int sockfd, int backlog);
返回值说明:
参数说明:
服务器监听
我们在创建完套接字和绑定之后 需要再进一步将状态设置为监听状态 监听后续是否有新的连接 如果监听失败就意味着TCP无法接受服务器发送的请求了 此时服务器也没有了启动的意义 直接退出即可
void Init()
{
_sockfd = socket(AF_INET, SOCK_STREAM , 0);
if (_sockfd < 0)
{
cout << "socket error" << endl;
exit(2);
}
struct sockaddr_in local;
memset(&local , 0 , sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_sockfd , (struct sockaddr*)&local , sizeof(sockaddr)) < 0)
{
cout << "bind error" << endl;
exit(3);
}
if(listen(_sockfd , 5) < 0)
{
cout << "listen error" << endl;
exit(4);
}
}
我们在初始化TCP服务器的时候只有在创建套接字完毕绑定成功
TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求。
accept函数
获取连接的函数叫做accept,该函数的函数原型如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值说明:
参数说明:
accept函数返回的套接字是什么?
调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。
监听套接字与accept函数返回的套接字的作用:
服务端获取连接
服务端在获取连接时需要注意:
void Start()
{
for(;;)
{
struct sockaddr_in peer;
memset(&peer , 0 , sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_sockfd , (struct sockaddr*)&peer , &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue; // do not stop server
}
string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
cout << "get a new link " << sock << "new port is " << client_port <<endl ;
}
}
服务端接受连接测试
我们现在做个测试 看看当前服务器能够接受请求
我们在服务器运行的时候传入一个端口号作为我们的服务端口 服务端初始化之后启动
编译代码后 我们使用8082端口号初始化服务器
服务端运行之后我们可以通过netstat命令查看服务
它绑定的端口就是8082 而由于服务器绑定的是INADDR_ANY 因此该服务器的本地IP地址是0.0.0.0 这就意味着该TCP服务器可以读取本地任何一张网卡里面的数据
此时最重要的是服务器状态处于listen状态
虽然我们现在还没有编写客户端相关的代码 但是我们现在已经能登录这个服务器了
我们可以使用telnet指令来登录当前服务器 因为itelntt指令底层就是使用tcp实现的
我们发现此时分配的文件描述符是4 这是因为在运行一个C++程序的时候默认会打开0 1 2 文件输入流 文件输出流 文件错误流
而3号文件描述符在初始化时分配给了监视套接字 因此当一个客户端发起连接请求的时候 为该客户端提供服务的文件套接字就是4
现在TCP服务器已经能够获取连接请求了 下面当然就是要对获取到的连接进行处理
但此时为客户端提供服务的不是监听套接字 因为监听套接字获取到一个连接后会继续获取下一个请求连接 为对应客户端提供服务的套接字实际是accept函数返回的套接字 下面就将其称为“服务套接字”
为了让通信双方都能看到对应的现象 我们这里就实现一个简单的回声TCP服务器 服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出 并且将客户端发来的数据重新发回给客户端即可
当客户端拿到服务端的响应数据后再将该数据进行打印输出 此时就能确保服务端和客户端能够正常通信了
read函数
TCP服务器读取数据的函数叫做read,该函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
返回值说明:
参数说明:
read返回值为0表示对端连接关闭
这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:
这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。
write函数
TCP服务器写入数据的函数叫做write,该函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
返回值说明:
参数说明:
当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。
服务端处理请求
需要注意的是,服务端读取数据是服务套接字中读取的,而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。
在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。
void Service(int sock)
{
char buff[1024];
while(true)
{
ssize_t size = read(sock , buff , sizeof(buff)-1);
if (size > 0)
{
buff[size] = 0;
write(sock , buff , size);
}
else if (size == 0) // 对端关闭
{
close(sock);
cout << "read cloes " << endl;
break;
}
else
{
cout << "error : " << errno << endl;
break;
}
}
}
同样的 我们也可以将客户端封装成一个类 当我们需要一个客户端的时候将其进行实例化
而我们客户端只需要创建套接字就可以 至于绑定和监听则不需要
为什么客户端不需要绑定和监听
此外 客户端必须要知道它要连接的服务端的IP地址和端口号 因此客户端除了要有自己的套接字之外 还需要知道服务端的IP地址和端口号 这样客户端才能够通过套接字向指定服务器进行通信
class TcpClient
{
private:
int _sockfd;
int _sever_port;
string _sever_ip;
public:
TcpClient(string ip , int port)
:_sever_ip(ip),
_sever_port(port),
_sockfd(-1)
{}
~TcpClient()
{
if(_sockfd >= 0)
{
close(_sockfd);
}
}
public:
void ClientInit()
{
_sockfd = socket(AF_INET , SOCK_STREAM , 0);
if (_sockfd < 0)
{
cout << "ClientInit error" << endl;
exit(1);
}
}
};
由于我们的客户端不需要绑定监听 所以当创建完毕之后就可以开始处理请求了 我们使用connect函数来处理请求
connect函数
该函数原型如下
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值说明:
参数说明:
客户端连接服务器
这里需要特别注意的一点是 客户端只是不自己绑定端口号和ip 并不是不需要端口号和ip
因为通信双方都必须要有IP地址和端口号 否则无法唯一标识通信双方
也就是说 如果connect函数调用成功了 客户端本地会随机给该客户端绑定一个端口号发送给对端服务器
但是我们进行连接的时候传入的并不是客户端的IP和PORT 而是服务器的 因为我们要连接的是服务器 在连接的时候客户端的IP和PORT会自动传给服务器
void ClientStart()
{
struct sockaddr_in peer;
memset(&peer , 0 , sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_sever_port);
peer.sin_addr.s_addr = inet_addr(_sever_ip.c_str());
if (connect(_sockfd , (struct sockaddr*)&peer , sizeof(peer)) == 0)
{
cout << "connect success" << endl;
// request
}
else
{
cout << "connect fail" << endl;
exit(2);
}
}
由于我们实现的是一个简单的回声服务器 因此当客户端连接到服务端之后就可以向服务端发送数据了 这里我们可以将客户端发送的数据利用write函数发送给服务端
当我们的客户端发送数据给服务端之后 由于服务端读取到数据后还会进行回显 因此客户端在发送数据后还需要调用read函数读取服务端的响应数据 然后将该响应数据进行打印以确定双方通信无误
void Request()
{
string msg;
char buff[1024];
while(true)
{
cout << "please enter#" << endl;
fflush(nullptr);
getline(cin , msg);
write(_sockfd , msg.c_str() , msg.size());
ssize_t size = read(_sockfd , buff , sizeof(buff) -1);
if(size > 0)
{
buff[size] = 0; // '\0'
cout << "sever echo: " << buff << endl ;
}
else if (size == 0)
{
cout << "sever exit " << endl ;
break;
}
else
{
cout << "sever error" << endl;
break;
}
}
close(_sockfd);
}
在运行客户端程序时我们就需要携带上服务端对应的IP地址和端口号 然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象 对客户端进行初始后启动客户端即可
我们首先启动服务器
之后使用netstat来查看网络状态
之后我们再运行客户端
我们发现简单的回声服务器就制作完毕了