目录
前言
上一章我们主要讲解了UDP之间的通信,本章我们将来讲述如何使用TCP来进行网络间通信,主要是使用socket API进行代码的实现。
我们一共讲了5个socket API接口,分别为**socket,bind,listen,accept,connect.**但我们在讲解UDP通信时,只使用了socket和bind这两个接口就完成了。而TCP通信会使用后面这三个接口,我们将分别讲解.
TCP通信流程
同样地,TCP通信分为服务器端和客户端,它们的流程分别如下:
服务端通信流程:
-
创建套接字 :使用
socket
函数创建一个套接字,指定协议族为AF_INET(IPv4)或AF_INET6(IPv6),指定类型为SOCK_STREAM(TCP)。 -
绑定套接字 :使用
bind
函数将套接字与服务器的IP地址和端口号绑定在一起。这样服务器将使用指定的IP地址和端口号进行监听。 -
监听连接请求 :使用
listen
函数开始监听连接请求。指定参数backlog,表示允许在队列中等待的最大连接数。 -
接受连接请求 :使用
accept
函数接受客户端的连接请求。该函数会阻塞程序,直到有客户端连接时才返回一个新的套接字 ,用于与客户端进行通信。(新的套接字和旧套接字区别:新套接字负责服务建立的连接,包括通信等,旧套接字则一直负责监听连接.) -
通信 :使用新的套接字进行通信。可以使用
read
和write
函数进行数据的接收和发送。 -
关闭连接 :当通信结束后,使用
close
函数关闭套接字,释放资源。
客户端通信流程:
-
创建套接字 :使用
socket
函数创建一个套接字,指定协议族为AF_INET(IPv4)或AF_INET6(IPv6),指定类型为SOCK_STREAM(TCP)。 -
连接服务器 :使用
connect
函数连接到服务器的IP地址和端口号。如果连接成功,返回0;否则返回错误码。 -
通信 :使用已连接的套接字进行数据的发送和接收,可以使用
read
和write
函数。 -
关闭连接 :当通信结束后,使用
close
函数关闭套接字,释放资源。
TCP通信的代码实现
依然是三个文件,分别为tcp_server.hpp (用来封装tcp socket),tcp_server.cc (服务器通信代码),tcp_client.cc(客户端通信代码).
tcp_server.hpp编写
首先我们要编写tcp_server.hpp, 首先第一个接口initServer初始化服务端. 一共分为三步:
- 1.创建套接字
利用socket函数创建新的套接字,并判断是否成功:
listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create sock success, listensock: %d", listensock);
- 2.bind绑定
bind将套接字和特定的ip和地址绑定在一起.用法我们上一章也说了,先创建一个sockaddr_in结构体,然后填入相关的数据:sin_family (协议族AF_INET (IPv4)或AF_INET6 (IPv6)),sin_port (端口号),sin_arr.s_addr(ip地址),然后再bind绑定并判断是否成功,代码如下:
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if (bind(listensock, (struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind error", errno, strerror(errno));
exit(3);
}
- 3.listen监听
listen监听是否有新的连接,TCP与UDP不同的是,当客户端和服务端正式通信的时候,需要先建立连接,而UDP直接发送数据。所以要listen来监听是否有新链接.
代码如下:
// 3.因为TCP是面向连接的,意味着当我们正式通信的时候,需要先建立连接
//第二个参数我们在讲TCP协议时会详细讲解,这里先暂且设为20
if (listen(listensock, gbacklog) < 0)
{
logMessage(FATAL, "listen error", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "init server success");
第二个接口Start(),该接口主要负责获取连接,并进行通信.共分为两步:
- accept获取到客户端连接
这个我们同样的需要创建一个sockaddr_in结构体,用来存储客户端的连接信息,然后接收新的套接字,这个套接字是接下来我们通信要使用的。
struct sockaddr_in src;
socklen_t len = sizeof src;
//servicesock(未来真正进行IO) vs listensock(主要任务:获取新链接)
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error", errno, strerror(errno));
}
- 通信流程
这里可以提供两个版本的:一个是单进程版,即每一次只能处理一个客户端.
另一个是 多进程版,通过创建子进程来实现对多个客户端处理.
- 单进程版
紧接着上面说的,我们获取到客户端的连接信息后,我们需要对其进行解析,得到其ip地址和端口号:
uint16_t client_port = ntohs(src.sin_port);//获得端口号
string client_ip = inet_ntoa(src.sin_addr);//获得ip
logMessage(NORMAL, "Link success, %d | %s : %d\n", servicesock, client_ip.c_str(), client_port);
然后直接执行对应的通信函数即可:
service(servicesock,client_ip,client_port);
- 多进程版:
利用fork函数实现,代码如下:后面的服务端通信和客户端通信都不用改动
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
close(listensock);
service(servicesock,client_ip,client_port);
exit(0);//僵尸状态
}
close(servicesock);
通信函数service的实现:我们从sock中读取消息,客户端没有发消息时,服务端会阻塞在这里等待用户的输入。
static void service(int sock,const string& clientip,const uint16_t& clientport)
{
//echo server
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
while(true)
{
//read && write
ssize_t s = read(sock,buffer,sizeof buffer-1);
if(s > 0)
{
buffer[s] = 0;//将发过来的数据当做字符串
cout << clientip << " : " << clientport << "# "<< buffer << endl;
}
else if(s== 0)//对端链接关闭
{
logMessage(NORMAL,"%s : %d shutdown, me too!",clientip.c_str(),clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock,buffer,strlen(buffer));
}
close(sock);
}
tcp_server.cc服务端的编写
这个就很简单了,只需要调用initServer初始化和Start开始就行了.
#include "tcp_server.hpp"
#include <memory>
static void usage(string proc)
{
cout << "Usage: " << proc << "ServerPort\n" << endl;
}
//./tcp_server port
int main(int argc, char* argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> svr(new TcpServer(port));
svr->initServer();
svr->Start();
return 0;
}
tcp_client.cc客户端的编写
-
创建套接字:
int sock = socket(AF_INET, SOCK_STREAM, 0);
-
调用connect与服务端链接:利用命令行参数,将用户输入的ip地址和port端口号获取到,然后传入sockaddr_in结构体,最后进行connect
uint16_t serverPort = atoi(argv[2]); string serverIp = argv[1]; struct sockaddr_in server; bzero(&server, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(serverPort); server.sin_addr.s_addr = inet_addr(serverIp.c_str()); if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
-
进行通信(send和recv)
TCP的发送和接收消息不同于UDP的sendto和recvfrom,而是send和recv。我们分别看一下函数的用法:
send:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd
:发送数据的套接字描述符。即想谁发送buf
:指向要发送数据的缓冲区的指针。len
:要发送的数据的长度(以字节为单位)。flags
:附加选项,通常设为0。作用:send()
函数用于将数据从发送端发送到接收端。它返回已发送的字节数,或者在出现错误时返回-1 。可以通过设置flags
参数来指定传输数据的特定选项,例如设置为MSG_DONTWAIT
非阻塞发送等。
recv:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd
:要接收数据的套接字描述符。即谁接收buf
:接收数据的缓冲区的指针。len
:接收数据的最大长度(以字节为单位)。flags
:附加选项,通常设为0。- 作用:
recv()
函数用于从套接字接收数据,并将其存储在指定的缓冲区中。它返回接收到的字节数,或者在出现错误时返回-1。 可以通过设置flags
参数来指定接收数据的特定选项,例如设置为MSG_DONTWAIT
非阻塞接收等。
所以通信代码如下:
while (true)
{
string line;
cout << "Please Enter Message# ";
getline(cin, line);
send(sock, line.c_str(), line.size(), 0);
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
cout << "server echo# " << buffer << endl;
}
else if (s == 0)
{
break;
}
else
{
break;
}
}
至此我们的TCP通信就完成了.
当我们使用多进程通信时,可以有多个客户端同时向服务端发送消息:
至此,TCP的网络通信流程也完成了,这是完整的代码,可以直接 拷贝运行,可去掉logMessage相关的调试信息.
整体代码
注意运行服务器时,使用**./tcp_server 端口号**
运行客户端连接服务器时,使用**./tcp_clinet 服务器ip 服务器端口号**
tcp_server.hpp文件
#pragma once #include <iostream> #include <stdlib.h> #include <assert.h> #include <unistd.h> #include <string.h> #include <memory> #include <pthread.h> #include <signal.h> #include <cstring> #include <ctype.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; static void service(int sock,const string& clientip,const uint16_t& clientport) { //echo server char buffer[1024]; memset(buffer, 0, sizeof(buffer)); while(true) { //read && write ssize_t s = read(sock,buffer,sizeof buffer-1); if(s > 0) { buffer[s] = 0;//将发过来的数据当做字符串 cout << clientip << " : " << clientport << "# "<< buffer << endl; } else if(s== 0)//对端链接关闭 { logMessage(NORMAL,"%s : %d shutdown, me too!",clientip.c_str(),clientport); break; } else { logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno)); break; } write(sock,buffer,strlen(buffer)); } } class TcpServer { public: const static int gbacklog = 20; TcpServer(uint16_t port, string ip = "") : _port(port), _ip(ip), listensock(-1) { } void initServer() { // 1.创建套接字 listensock = socket(AF_INET, SOCK_STREAM, 0); if (listensock < 0) { logMessage(FATAL, "%d:%s", errno, strerror(errno)); exit(2); } logMessage(NORMAL, "create sock success, listensock: %d", listensock); // 2.bind struct sockaddr_in local; memset(&local, 0, sizeof local); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); if (bind(listensock, (struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind error", errno, strerror(errno)); exit(3); } // 3.因为TCP是面向连接的,意味着当我们正式通信的时候,需要先建立连接 if (listen(listensock, gbacklog) < 0) { logMessage(FATAL, "listen error", errno, strerror(errno)); exit(3); } logMessage(NORMAL, "init server success"); } void Start() { //version2 :signal(SIGCHLD,SIG_IGN); //对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸进程 while (true) { // sleep(1); // 获取连接 struct sockaddr_in src; socklen_t len = sizeof src; // sock(未来真正进行IO) and _sock(主要任务:获取新链接) int servicesock = accept(listensock, (struct sockaddr *)&src, &len); if (servicesock < 0) { logMessage(ERROR, "accept error", errno, strerror(errno)); } // 获取连接成功 uint16_t client_port = ntohs(src.sin_port); string client_ip = inet_ntoa(src.sin_addr); logMessage(NORMAL, "Link success, %d | %s : %d\n", servicesock, client_ip.c_str(), client_port); // 开始进行通信服务 // version 1 -- 单进程循环 -- 只能一次处理一个客户端,处理完一个,才能处理下一个 // 显然是不能被直接使用的?为什么?单进程. service(servicesock,client_ip,client_port); // version 2 -- 多进程版本 -- 创建子进程, // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? 答案是当然可以! pid_t id = fork(); assert(id != -1); if(id == 0) { //子进程 close(listensock); service(servicesock,client_ip,client_port); exit(0);//僵尸状态 } //父进程 close(servicesock); } } ~TcpServer() { } private: uint16_t _port; string _ip; int listensock; unique_ptr<ThreadPool<Task>> _threadpool_ptr; };
tcp_server.cc文件
#include "tcp_server.hpp" #include <memory> static void usage(string proc) { cout << "Usage: " << proc << "ServerPort\n" << endl; } //./tcp_server port int main(int argc, char* argv[]) { if(argc != 2) { usage(argv[0]); exit(1); } uint16_t port = atoi(argv[1]); unique_ptr<TcpServer> svr(new TcpServer(port)); svr->initServer(); svr->Start(); return 0; }
cline.cc文件
#include <iostream> #include <string> #include <cstdio> #include <unistd.h> #include <strings.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; static void usage(string proc) { cout << "Usage: " << proc << "ServerIP ServerPort" << endl; } // ./tcp_clinet IP Prot int main(int argc, char *argv[]) { if (argc != 3) { usage(argv[0]); exit(-1); } uint16_t serverPort = atoi(argv[2]); string serverIp = argv[1]; int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { cerr << "sokcet error" << endl; exit(2); } // client 不需要显式的bind,OS会自动选择 // 更不需要监听,但是需要连接的能力connect struct sockaddr_in server; bzero(&server, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(serverPort); server.sin_addr.s_addr = inet_addr(serverIp.c_str()); if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0) { cerr << "connect error" << endl; exit(3); } cout << "connect success!" << endl; while (true) { string line; cout << "Please Enter Message# "; getline(cin, line); send(sock, line.c_str(), line.size(), 0); char buffer[1024]; ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); if (s > 0) { buffer[s] = 0; cout << "server echo# " << buffer << endl; } else if (s == 0) { break; } else { break; } } close(sock); return 0; }