TCP 通信基本流程
不管多么复杂的服务器或客户端程序,其网络通信的基本原理一定如下所述:
对于服务器,其通信流程一般有如下步骤:
1. 调用 socket 函数创建 socket(侦听socket)
2. 调用 bind 函数 将 socket绑定到某个ip和端口的二元组上
3. 调用 listen 函数 开启侦听
4. 当有客户端请求连接上来后,调用 accept 函数接受连接,产生一个新的 socket(客户端 socket)
5. 基于新产生的 socket 调用 send 或 recv 函数开始与客户端进行数据交流
6. 通信结束后,调用 close 函数关闭侦听 socket
对于客户端,其通信流程一般有如下步骤:
1. 调用 socket函数创建客户端 socket
2. 调用 connect 函数尝试连接服务器
3. 连接成功以后调用 send 或 recv 函数开始与服务器进行数据交流
4. 通信结束后,调用 close 函数关闭侦听socket
上述流程可以绘制成如下图示:
对于上面的图,读者可能有疑问,为什么客户端调用 close() ,会和服务器端 recv() 函数有关。这个涉及到 recv() 函数的返回值意义,我们在下文中详细讲解。
服务器端实现代码:
/**
* TCP服务器通信基本流程
* GHzbq 2019.05.07
*/
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char* argv[])
{
// 0. 在启动服务器之前做一点准备工作
// 服务器一般是要绑定 ip 和 port 的
// 服务器的启动方式为 ./server ip port
if(argc != 3)
{
std::cout << "Usage: " << argv[0] << " ip + port" << std::endl;
return 0;
}
// 1. 创建监听套接字
int listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listenSock < 0)
{
std::cerr << "socket() error" << std::endl;
exit(-1);
}
// 2. 初始化服务器地址
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
socklen_t serverAddrLen = sizeof(sockaddr_in);
int ret = bind(listenSock, (sockaddr*)&serverAddr, serverAddrLen);
if(ret < 0)
{
std::cerr << "bind() error " << std::endl;
close(listenSock);
exit(-1);
}
// 3. 启动侦听
ret = listen(listenSock, 5);
if(ret < 0)
{
std::cerr << "listen() error" << std::endl;
close(listenSock);
exit(-1);
}
char buf[BUFSIZ] = {0};
ssize_t ret_recv = 0;
ssize_t ret_send = 0;
for(; ;)
{
sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(sockaddr_in);
// 4. 接收客户端连接
int clientSock = accept(listenSock, (sockaddr*)&clientAddr, &clientAddrLen);
if(clientSock < 0)
{
std::cerr << "accept() error" << std::endl;
break;
}
for(; ; )
{
memset(buf, 0x00, BUFSIZ);
// 5. 从客户端接收数据
ret_recv = recv(clientSock, buf, BUFSIZ-1, 0);
if(ret_recv > 0)
{
buf[ret_recv] = '\0';
std::cout << "recv data from client, data: " << buf << std::endl;
// 6. 将收到的数据原封不动的发给客户端
ret_send = send(clientSock, buf, ret_recv, 0);
if(ret_send < 0 || ret_send != ret_recv)
{
std::cerr << "send data error." << std::endl;
break;
}
}
else if(ret_recv == 0)
{
std::cout << "peer close connection." << std::endl;
break;
}
else
{
std::cerr << "recv data error." << std::endl;
break;
}
}
close(clientSock);
}
// 7. 关闭侦听socket
close(listenSock);
return 0;
}
客户端实现代码:
/**
* TCP客户端通信基本流程
* GHzbq 2019.5.7
* */
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static const char* data = "hello world";
static const ssize_t len = strlen(data);
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "Usage: " << argv[0] << " ip + port" << std::endl;
return 0;
}
int clientSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(clientSock < 0)
{
std::cerr << "socket() error" << std::endl;
exit(-1);
}
sockaddr_in serverAddr;
socklen_t serverAddrLen = sizeof(sockaddr_in);
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
serverAddr.sin_port = htons(atoi(argv[2]));
int ret = connect(clientSock, (sockaddr*)&serverAddr, serverAddrLen);
if(ret < 0)
{
std::cerr << "connect() error" << std::endl;
close(clientSock);
exit(-1);
}
char buf[BUFSIZ] = {0};
ssize_t ret_send = 0;
ssize_t ret_recv = 0;
for(; ; )
{
ret_send = send(clientSock, data, len, 0);
if(ret_send != len)
{
std::cerr << "send() data error" << std::endl;
break;
}
memset(buf, 0x00, BUFSIZ);
ret_recv = recv(clientSock, buf, BUFSIZ-1, 0);
if(ret_recv > 0)
{
buf[ret_recv] = '\0';
std::cout << "recv data successfully, data: " << buf << std::endl;
}
else if(ret_recv == 0)
{
std::cerr << "peer close connection" << std::endl;
break;
}
else
{
std::cerr << "recv error" << std::endl;
break;
}
sleep(3);
}
close (clientSock);
return 0;
}
以上代码,服务器端在地址 127.0.0.1:9092
启动一个侦听,客户端连接服务器成功后,给服务器发送字符串"hello world"
;服务器收到后,将收到的字符串原封不动地发给客户端。
为方便管理,我们写个简单的 makefile
,内容如下:
all: server client
server: ./server.cpp
g++ server.cpp -o $@ -std=c++11
client: ./client.cpp
g++ client.cpp -o $@ -std=c++11
.PHONY: clean
clean: server client
rm -f $^
写完 makefile
,我们只需要 make
一下,就可以得到两个可执行文件:
[root@izuf68phfbhbm8bn16zrhrz tcp]# make
g++ server.cpp -o server -std=c++11
g++ client.cpp -o client -std=c++11
接下来,我们启动服务器端和客户端:
[root@izuf68phfbhbm8bn16zrhrz tcp]# ./server 127.0.0.1 9092
[root@izuf68phfbhbm8bn16zrhrz tcp]# ./client 127.0.0.1 9092
之后服务器就和客户端互发消息了,没3秒发一次:
以上就是 TCP socket 网络通信的基本原理,对于很多读者来说,上述代码可能很简单,更有点“玩具”的意味。但是深刻理解这两个代码片段是进一步学习开发复杂的网络通信程序的基础。而且看似很简单的代码,却隐藏了很多的玄机和原理。