深入了解linux网络—— TCP网络通信(上)
前言
了解了UDP
通信相关接口,现在来学习TCP
通信的相关接口
服务端
无论是UDP
通信,还是TCP
通信;都要创建套接字、绑定端口号。
1. 初始化
创建套接字
int socket(int domain, int type, int protocol);
这里要使用TCP
通信,传递的参数就应该是:AF_INET
、SOCKSTREAM
(面向字节流)
socket(AF_INET,SOCK_STREAM,0); //AF_INET 网络通信 SOCK_STREAM 面向字节流
对于socket
返回值,是一个文件描述符;和UDP
使用略有差别(在accept
详细介绍)
绑定端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
对于bind
绑定端口号,需要sockfd
和struct sockaddr*
类型的指针对象;(这里就直接使用封装好的InetAddr
具体是实现在:lesson17/chat/inetaddr.hpp · 迟来的grown/linux)
bind(_sockfd, addr.GetInetAddr(), addr.GetLen());
这里服务端,IP地址就直接绑定
INADDR_ANY
了
监听状态
对于
UDP
通信,只需要创建套接字、绑定端口号就可以了;而
TCP
通信,除此之外还需要设置监听状态
设置监听状态要是有接口 : listen
int listen(int sockfd, int backlog);
参数:
sockfd
:创建套接字socket
返回的文件描述符。backlog
:表示该套接字维护的连接请求队列的最大长度,也就是:等待被接受的最大连接数返回值:
成功返回
0
、失败则返回-1
。
所以,对于UDP
通信,只需要创建套接字和绑定端口号;
而TCP
通信,还需要设置监听状态。
class TcpServer
{
public:TcpServer(uint16_t port) : _sockfd(-1), _port(port){}~TcpServer() {}void Init(){// 1. socket_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(Level::FATAL) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success";// 2. bindInetAddr addr(_port);int b = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());if (b < 0){LOG(Level::FATAL) << "bind error";exit(2);}LOG(Level::DEBUG) << "bind success";// 3. listenint l = listen(_sockfd, 5);if (l < 0){LOG(Level::FATAL) << "listen error";exit(3);}LOG(Level::DEBUG) << "listen success";}
private:int _sockfd;uint16_t _port;
};
这样,在使用时,只需创建TcpServer
对象,直接调用即可:
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "usage : " << argv[0] << " port" << std::endl;exit(1);}uint16_t port = std::stoi(argv[1]);TcpServer tsvr(port);tsvr.Init();sleep(100);return 0;
}
这里,我们可以使用
netstat
命令查看:(ntestat -naltp
)
-l
: 监听状态;-t
:TCP通信
2. 读取消息
在UDP
通信中,创建套接字、绑定端口号之后,就可以直接调用sendto
和recvfrom
进行发送和接受信息。
而在TCP
中,进行读取消息之前,还需要建立连接;(服务端获取连接请求,客户端发送连接请求)。
只有建立了连接,才能进行网络通信。
获取连接
客户端获取连接请求所有到的接口:accept
int accept(int sockfd, struct sockaddr *_Nullable restrict addr,socklen_t *_Nullable restrict addrlen);
参数
参数相对来说还是非常好理解的:
sockfd
创建套接字所返回的文件描述符;addr
:输出型参数,获取远端的addr
addrlen
传参时表示addr
的长度,调用成功后表示所获取到远端addr
的长度。
返回值:
对于accept
的返回值就非常有意思了:
看看到,如果调用成功,返回一个文件描述符;那accept
返回的文件描述符和socket
返回的文件描述符有什么区别呢?
socket
:创建套接字返回的文件描述符,该文件描述符只用来绑定端口号和获取连接请求。
accept
:对于accept
返回的文件描述符,在通信时读取使用。
简单来说就是,一个服务端可能连接多个客户端;
每一个连接都存在一个文件描述符,在服务的通过文件描述符来 接受/发送信息 给客户端。
接受/发送信息
对于TCP
通信,要接受信息用的接口是read
;(就是进行文件读操作的read
)
而发送信息用的接口是write
;(就是文件写操作的write
)
要读取信息,用的就是accept
返回的文件描述符
void Server(int rwfd){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << "read : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 读写Server(rwfd);}}
这里读写操作和文件读写一模一样。
简单来说:
TCP
通信,socket
返回的文件描述符只用来绑定bind
、监听listen
和获取连接请求accept
使用;而进行通信使用的都是
accept
返回的文件描述符。
3. telnet 测试
这里只是实现了server
端代码,简单测试一下;
telnet
命令可以用来连接某IP地址端口号(发送连接请求)
telnet IP port
这里可以使用netstat -natlp
查看连接情况:
这里是在一台服务器上做测试,可以看到两条连接。
客户端
对于客户端,首先还是要创建套接字;
还是无需显示绑定(UDP
通信时,是首次发送信息时绑定;那TCP
呢?)
在TCP
通信中,则是在connect
成功时自动绑定。
所以,客户端除了创建套接字之外,还需要做的就是发送连接请求;
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
对于connect
的参数,还是非常容易理解的:
sockfd
:创建套接字返回的文件描述符。addr
:远端的sockaddr_in
字段。addrlen
:addr
的长度
返回值:
客户端绑定是在
connect
成功时自动绑定的;
connect
成功/绑定成功,返回0
;否则返回-1
,且错误码被设置。
读写操作
对于客户端读写操作,还是使用
read
、write
接口;所用的文件描述符就是
socket
返回的文件描述符。
#include "log.hpp"
#include "inetaddr.hpp"
using namespace hllog;
using namespace hladdr;
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "usage : " << argv[0] << " server_ip server_port" << std::endl;exit(1);}InetAddr server(argv[1], std::stoi(argv[2]));// 1. socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){LOG(Level::FATAL) << "socket error";exit(1);}LOG(Level::INFO) << "socket success, sockfd : " << sockfd;// 2. connectint n = connect(sockfd, server.GetInetAddr(), server.GetLen());if (n < 0){LOG(Level::FATAL) << "connect error";exit(2);}LOG(Level::INFO) << "connect success, sockfd : " << sockfd;// 写/读while (true){std::string massage;std::cout << "Please Enter #";std::getline(std::cin, massage);int wn = write(sockfd, massage.c_str(), massage.size());if (wn < 0){LOG(Level::WARNING) << "write error";break;}// 接受char buff[256];int rn = read(sockfd, buff, sizeof(buff) - 1);if (rn < 0)continue;else if (rn == 0)break;buff[rn] = '\0';std::cout << "recive : " << buff << std::endl;}return 0;
}
多进程
对于上述实现的代码,存在一个bug
:一次只能处理一个client
端的请求;
这是因为在server
获取到一个连接时,就会长服务式的处理这个请求(读写);只要这个连接不退出,server
就无法获取新的连接请求。
这里就将上述代码修改成多进程的:
在
server
端获取到一个连接时,就创建一个子进程,让子进程去服务;父进程继续获取请求。问题:子进程退出时,父进程如何回收?何时回收?
- 解决方案1:将
tcpserver
进程对SIGCHLD
信号的处理方式设置成SIG_IGN
或者自定义捕捉。- 解决方案2 :子进程再创建子进程(孙子进程),然后子进程退出,
tcpserver
回收子进程;孙子进程去服务(孤儿进程,进程推了操作系统自动回收)。对于多进程要注意:在创建子进程后要关闭不用的文件描述符。
这里就直接实现方案二:
void Server(int rwfd){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << "read : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 读写// Server(rwfd);// 多进程pid_t pid = fork();if (pid < 0){LOG(Level::FATAL) << "fork error";exit(1);}else if (pid == 0){close(_sockfd);if (fork() == 0)Server(rwfd);exit(0);}// 父进程waitpid(pid, nullptr, 0);}}
多线程
要实现多线程版本,创建线程去执行Server
;
但是Server
的是void(TcpServer*, int)
类型的,创建线程要执行的方法:void*(void*)
类型。
并且,创建出来的线程是不知道通信要使用的文件描述符的。
对于线程无法访问到通信要使用的文件描述符,这里直接在
TcpServer
中使用一个类,表示线程调用Server
需要的数据。需要哪些数据呢?(
fd
读写使用的文件描述符、TcpServer*
类型的指针对象用来调用Server
方法,要知道远端通信对方的IP地址和port,也需要InetAddr
类型对象。)对于线程执行方法,使用一个静态成员方法
Routinue
,通过传参将所需要的数据传递进去。在创建完线程之后,设置新线程
detach
分离,无需手动回收
void Server(int rwfd, InetAddr &addr){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << addr.ToString() << " : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}class ThreadData{public:ThreadData(int fd, TcpServer *tsvr, InetAddr addr): _fd(fd), _tsvr(tsvr), _addr(addr){}int _fd;TcpServer *_tsvr;InetAddr _addr;};static void *Routinue(void *argv){ThreadData *td = static_cast<ThreadData *>(argv);td->_tsvr->Server(td->_fd, td->_addr);return nullptr;}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 多线程pthread_t tid;ThreadData* td = new ThreadData(rwfd,this,peer);pthread_create(&tid,nullptr,Routinue,td);}}
到这里,本篇文章内容就结束了,感谢支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws