Linux网络:使用TCP实现网络通信(服务端)
文章目录
- 1. TCP网络程序的服务端(初始化)
- 1.1 封装一个TcpServer类
- 1.2 创建socket套接字
- 1.3 绑定套接字
- 1.4 监听套接字
- 2. TCP网络程序的服务端(连接和通信)
- 2.1 Accept建立连接
- 2.2 提取客户端信息
- 2.3 实现通信服务
-
序:在上以章中,我们详细介绍了UDP网络编程的核心内容,包括服务端和客户端的实现。服务端和客户端重点讲解了如何使用
recvfrom
接收数据和sendto
发送数据,以及如何处理客户端地址信息;此外,还探讨了IP与端口相关知识,如公网IP绑定限制、知名端口范围等。而本篇文章将讨论TCP网络编程,使用TCP来实现网络通信,看看TCP的服务端与客户端与UDP又有怎样的差距和变化。 -
补充:
在上一章中,我们完成了使用UDP时限网络通信的程序,我们就能用此来建造一个类似于qq群聊的聊天室,由于在Linux中一切皆文件,/dev/pts/目录下存放的就是多个终端的文件,于是我们就可以做到将程序运行的结果在其他终端打印。
有了这个,我们就可以将所有客户端的信息都打印到同一个终端中。
1. TCP网络程序的服务端(初始化)
1.1 封装一个TcpServer类
要想启动服务端,服务端至少要一个构造,一个析构,一个初始化和一个运行的接口!!!
const int defualtfd = -1;class TcpServer
{
public:TcpServer():_listensock(defualtfd){}void InitServer(){}void Start(){}~TcpServer(){}
private:int _listensock;
};
1.2 创建socket套接字
想要使用TCP套接字,就必须先获取一个套接字,要用到
socket
函数来获取套接字,与获取UDP套接字不同,在获取TCP套接字的时候,第二个参数要使用字节流的选项,而非用户数据报,在上一章中,我们就说过TCP是面向字节流的,所以第二个选项选择SOCK_STREAM
,第三个参数依旧填0。
class TcpServer
{
public:TcpServer(const uint16_t &port,const std::string &ip=defualtip):_listensockfd(defualtfd),_ip(ip),_port(port){}void InitServer(){_listensockfd = socket(AF_INET,SOCK_STREAM,0);if(_listensockfd < 0){lg(Fatal,"create sockfd error,erron: %d,strerror: %s",errno,strerror(errno));exit(SOCKET_ERR);}lg(Info,"create sockfd success, sockfd: %d",_listensockfd);}
private:int _listensock;uint16_t _port;std::string _ip;
};
1.3 绑定套接字
其中要绑定IP地址,要将点分十进制的字符串转化为
in_addr
的函数:inet_aton
要包含的头文件:
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
该函数的第一个参数是传入要转化的字符串。
第二个参数是传入一个uint32_t
的四字节地址
const std::string defualtip ="0.0.0.0";
const uint16_t defualtport =1234;class TcpServer
{
public:TcpServer(const uint16_t &port,const std::string &ip=defualtip):_listensockfd(defualtfd),_ip(ip),_port(port){}void InitServer(){//创建套接字//...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));//local.sin_addr.s_addr = INADDR_ANY;if(bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0){lg(Fatal,"bind error,erron: %d,strerror: %s",errno,strerror(errno));exit(BIND_ERR);}lg(Info,"bind success, sockfd: %d",_listensockfd);}
private:int _listensock;uint16_t _port;std::string _ip;
};
1.4 监听套接字
到了这一步,TCP与UDP的不同就显现出来了,我们知道UDP是无连接的,而TCP是有连接的,也就是说,tcp在通信前,要先建立连接,所以要将自己的套接字变成监听状态,Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直等待连接到来的状态,这样,当有客户端想要来连接是,服务端就能够连接到。
使用listen函数将套接字变成监听状态:
该函数的第一个参数表示要变成监听状态的套接字
第二个参数表示底层全连接的长度
RETURN VALUE返回值:
成功就返回0,失败就返回-1,并将错误码设置
class TcpServer
{
public:void InitServer(){//创建,绑定套接字//...//Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直等待连接到来的状态if(listen(_listensockfd,backlog) < 0){lg(Fatal,"listen error,erron: %d,strerror: %s",errno,strerror(errno));exit(LISTEN_ERR);}lg(Info,"listen success, sockfd: %d",_listensockfd);}
private:int _listensock;uint16_t _port;std::string _ip;
};
2. TCP网络程序的服务端(连接和通信)
2.1 Accept建立连接
因为TCP是面向连接的,所以在实现通信之前要先把连接建立起来,然后再根据连接进行通信,所以我们就要用到建立连接的函数
accept
:
第一个参数:当前服务器设置为监听状态的套接字
第二和第三个参数:输出型参数,用来获取客户端的IP地址和端口号等信息,就可以知道是谁发的,标识客户端的唯一性
RETURN VALUE返回值:
关键在于accept
的返回值,成功则返回一个文件描述符,失败则返回-1,错误码被设置
其中的listensock只是为了将连接从底层给到
accept
,真正实行网络通信的是accept
后的sockfd
!!!就好比一群学生去饭店吃饭,那个给学生们招呼进来的门口的宣传的店员就是listensock
,他只负责将顾客拉进店里,其他的不管,拉进店里后再又其他的服务员,也就是sockfd
,给他们安排座位,上菜等!!!
2.2 提取客户端信息
之前我们说了,accept能将客户端的信息提取出来,现在我们要将网络序列转化为主机序列了,端口号要从网络序列变成主机序列,IP地址要从4字节整数变成字符串。
其中的难点在于将IP地址从4字节整数变成字符串,在UDP的通信过程中我们是用
inet_ntoa
函数来实现转化的,但是该函数本身的使用可能会有线程安全的问题,因为man手册上说,inet_ntoa
函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。但是如果我们调用多次这个函数,为inet_ntoa
把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
所以我们这次使用一个新的函数
inet_ntop
:
该函数的第一个参数:对应的协议家族
第二个参数:对应的要转为字符串的四字节地址
第三个参数:用户自己传入一个缓冲区用来存放该字符串
第四个参数:传入的缓冲区的大小
class TcpServer
{
public:void Start(){//singal(SIGCHLD,SIG_IGN);lg(Info,"TcpServer is running");while(true){//1.获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);//没有接收到就是阻塞的int sockfd = accept(_listensockfd,(struct sockaddr*)&client,&len);if(sockfd < 0){lg(Fatal,"create sockfd error,erron: %d,strerror: %s",errno,strerror(errno));continue; }uint16_t clientport = ntohs(client.sin_port);//std::string clientip = inet_ntoa(client.sin_addr);char clientip[32]; inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));//2.根据新连接来进行通信lg(Info,"get a new link...,sockfd: %d,client ip: %s,client port: %d\n",sockfd,clientip,clientport);Service(sockfd,clientip,clientport);close(sockfd);}}
private:int _listensock;uint16_t _port;std::string _ip;
};
问题一:我们在实现网络通信的过程中,我们将IP地址和端口号都进行的网络序列的转化,但是,我们没有对要发送的内容进行网络序列的转化,为什么IP地址和端口号需要,而发送的内容不需要?
在套接字里面,正常的通信内容,我们所使用的接口,他会帮我们将要发送的数据进行主机序列转网络序列,我们不需要担心,至于为什么我们要手动将IP地址和端口号进行主机序列转网络序列,是因为,IP地址和端口号比较特殊是要给操作系统的,是需要我们手动去转的,所以在实际操作中我们是不需要考虑数据的大小端问题的!!!
2.3 实现通信服务
由于TCP是面向字节流的,所以,读取数据和写入数据直接用read和write就行了
class TcpServer
{
public:void Service(int sockfd,const std::string &clientip,const uint16_t &clientport){while(true){//因为tcp是面向字节流的,所以直接用read就可以直接接收到消息char buffer[4096];ssize_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 say@: ";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,client ip: %s,client port: %d",sockfd,clientip.c_str(),clientport);break;}}}
private:int _listensock;uint16_t _port;std::string _ip;
};
总结:
本文围绕TCP网络编程核心流程展开,先封装
TcpServer
类实现服务端初始化(创建socket、绑定地址、监听连接),再讲解accept
建立连接、inet_ntop
解析客户端信息,最后通过read/write
完成面向字节流的通信,完整呈现TCP服务端从搭建到交互的全流程,兼顾代码实操与原理阐释。