前言
了解了UDP
通信相关接口,现在来学习TCP
通信的相关接口
服务端
无论是UDP
通信,还是TCP
通信;都要创建套接字、绑定端口号。
1. 初始化
创建套接字
c
int socket(int domain, int type, int protocol);
这里要使用TCP
通信,传递的参数就应该是:AF_INET
、SOCKSTREAM
(面向字节流)
c
socket(AF_INET,SOCK_STREAM,0); //AF_INET 网络通信 SOCK_STREAM 面向字节流
对于socket
返回值,是一个文件描述符;和UDP
使用略有差别(在accept
详细介绍)
绑定端口号
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
对于bind
绑定端口号,需要sockfd
和struct sockaddr*
类型的指针对象;(这里就直接使用封装好的InetAddr
具体是实现在:lesson17/chat/inetaddr.hpp · 迟来的grown/linux)
c
bind(_sockfd, addr.GetInetAddr(), addr.GetLen());
这里服务端,IP地址就直接绑定
INADDR_ANY
了
监听状态
对于
UDP
通信,只需要创建套接字、绑定端口号就可以了;而
TCP
通信,除此之外还需要设置监听状态
设置监听状态要是有接口 : listen
c
int listen(int sockfd, int backlog);
参数:
sockfd
:创建套接字socket
返回的文件描述符。backlog
:表示该套接字维护的连接请求队列的最大长度,也就是:等待被接受的最大连接数返回值:
成功返回
0
、失败则返回-1
。
所以,对于UDP
通信,只需要创建套接字和绑定端口号;
而TCP
通信,还需要设置监听状态。
cpp
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. bind
InetAddr 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. listen
int 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
对象,直接调用即可:
cpp
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

c
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
返回的文件描述符
cpp
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
成功时自动绑定。
所以,客户端除了创建套接字之外,还需要做的就是发送连接请求;
cpp
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
返回的文件描述符。
cpp
#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. socket
int 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. connect
int 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
回收子进程;孙子进程去服务(孤儿进程,进程推了操作系统自动回收)。对于多进程要注意:在创建子进程后要关闭不用的文件描述符。
这里就直接实现方案二:
cpp
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
分离,无需手动回收
cpp
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