您现在的位置是:首页 >技术教程 >【网络编程】demo版TCP网络服务器实现网站首页技术教程
【网络编程】demo版TCP网络服务器实现
文章目录
一、引入
UDP和TCP的区别:
对于TCP协议有几个特点:
1️⃣ 传输层协议
2️⃣ 有连接(正式通信前要先建立连接)
3️⃣ 可靠传输(在内部帮我们做可靠传输工作)
4️⃣ 面向字节流
对于UDP协议有几个特点:
1️⃣ 传输层协议
2️⃣ 无连接
3️⃣ 不可靠传输
4️⃣ 面向数据报
可以看到TCP对比UDP会建立链接。
其他的接口跟UDP其实没什么区别:【网络编程】demo版UDP网络服务器实现
二、服务端实现
2.1 创建套接字socket
在通信之前要先把网卡文件打开。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
这个函数的作用是打开一个文件,把文件和网卡关联起来。
参数介绍:
domain
:一个域,标识了这个套接字的通信类型(网络或者本地)。
只用关注上面两个类,第一个AF_UNIX
表示本地通信,而AF_INET
表示网络通信。
type
:套接字提供服务的类型。
这一章我们讲的式TCP,所以使用SOCK_STREAM
。
protocol
:想使用的协议,默认为0即可,因为前面的两个参数决定了,就已经决定了是TCP还是UDP协议了。
返回值:
成功则返回打开的文件描述符(指向网卡文件),失败返回-1。
而从这里我们就联想到系统中的文件操作,未来各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
class TCPServer
{
static const uint16_t gport = 8080;
public:
TCPServer(cosnt uint16_t& port = gport)
: _sock(-1)
, _port(port)
{}
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
}
void start()
{}
private:
int _sock;
uint16_t _port;
};
2.2 绑定bind
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
RETURN VALUE
Upon successful completion, bind() shall return 0;
otherwise, -1 shall be returned and errno set to indicate the error.
参数介绍:
socket
:创建套接字的返回值。
address
:通用结构体(【网络编程】socket套接字有详细介绍)。
address_len
:传入结构体的长度。
所以我们要先定义一个sockaddr_in
结构体填充数据,在传递进去。
然后就是跟UDP一样,先初始化结构体,再处理IP和端口。
要注意IP要绑定任意IP也就是INADDR_ANY
。
至于为什么再上一章【网络编程】demo版UDP网络服务器实现有过详细讲解。
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
struct sockaddr_in si;
// 初始化结构体
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_port);// 主机转网络序列
si.sin_addr.s_addr = INADDR_ANY;
if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "bind socket error" << std::endl;
exit(1);
}
std::cout << "bind socket success" << std::endl;
}
2.3 设置监听状态listen
TCP跟UDP的不同在这里就体现了出来。
要把socket套接字的状态设置为listen状态。只有这样才能一直获取新链接,接收新的链接请求。
举个例子:
我们买东西如果出现了问题会去找客服,如果客服不在那么就回复不了,所以规定了客服在工作的时候必须要时刻接收回复消息,这个客服所处的状态就叫做监听状态。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
RETURN VALUE
On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.
关于第二个参数backlog后边讲TCP协议的时候介绍,目前先直接用。
static const int gbacklog = 10;
void InitServer()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock == -1)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
std::cout << "create socket success" << std::endl;
struct sockaddr_in si;
// 初始化结构体
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_port);// 主机转网络序列
si.sin_addr.s_addr = INADDR_ANY;
if(bind(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "bind socket error" << std::endl;
exit(1);
}
std::cout << "bind socket success" << std::endl;
// 设置监听状态
if(listen(_sock, gbacklog) < 0)
{
std::cout << "listen socket error" << std::endl;
exit(1);
}
std::cout << "listen socket success" << std::endl;
}
2.4 获取新链接accept
上面初始化完毕,现在开始就是要运行服务端,而TCP不能直接发数据,因为它是面向链接的,必须要先建立链接。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
RETURN VALUE
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket.
On error, -1 is returned, and errno is set appropriately.
参数介绍:
sockfd
文件描述符,找到套接字
addr
输入输出型参数,是一个结构体,用来获取客户端的信息。
addrlen
输入输出型参数,客户端传过来的结构体大小。
返回值:
成功返回一个文件描述符
失败返回-1
而我们知道sockfd本来就是一个文件描述符,那么这个返回的文件描述符是什么呢?
举个例子:
我们去吃饭的时候会发现每个店铺门口都会有人来招揽顾客,这个人把我们领进去门店后,然后他就会继续站在门口继续招揽顾客,而我们会有里面的服务员来招待我们,给我们提供服务。
这里的揽客的人就是sockfd,而服务员就是返回值的文件描述符。
意思就是sockfd的作用就是把链接从底层获取上来,返回值的作用就是跟客户端通信。
从这里就知道了成员变量中的_sock
并不是通信用的套接字,而是获取链接的套接字。为了方便观察,我们可以把所有的_sock换
成_listensock
。
void start()
{
while(1)
{
// 获取新链接
struct sockaddr_in si;
socklen_t len = sizeof si;
int sock = accept(_listensock, (struct sockaddr*)&si, &len);
if(sock < 0)
{
// 获取链接失败无影响,继续获取即可
std::cout << "accept error, continue" << std::endl;
continue;
}
std::cout << "accept a new link success" << std::endl;
std::cout << "sock: " << sock << std::endl;
}
}
2.5 获取信息与返回信息(文件操作)
上面获取到了通信用的套接字sock,而因为TCP通信是面向字节流的,所以后续通信全部是用文件操作(IO),因为文件也是面向字节流的。
IO的操作我们可以封装一个函数。
void ServerIO(int sock)
{
char buf[1024];
// 接收消息
while(1)
{
ssize_t n = read(sock, buf, sizeof buf - 1);
if(n > 0)
{
buf[n] = '