文章目录
1 🍑简单理解TCP/UDP协议 🍑
TCP协议:
- 1️⃣传输层协议
- 2️⃣有连接
- 3️⃣可靠传输
- 4️⃣面向字节流
UDP协议:
- 1️⃣传输层协议
- 2️⃣无连接
- 3️⃣不可靠传输
- 4️⃣面向数据报
2 🍑网络字节序 🍑
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可.
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
3 🍑socket编程接口 🍑
3.1 🍎socket 常见API 🍎
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
3.2 🍎sockaddr结构🍎
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
所以当我们使用的时候可以将地址强转成 sockaddr* 类型。
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr *; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
struct sockaddr的定义:
struct sockaddr_in的定义:
4 🍑简单的UDP网络程序 🍑
4.1 🍎基本分析🍎
在写之前,我们先来简单的分析分析下我们应该怎样写?首先我们封装一个udpServer的类来帮助我们创建套接字以及套接字的初始换工作,当然客户端也可以使用这种方式来完,不过由于客户端的代码很简单,我就不在封装一个udpClient的类了。
其次我们思考下udpServer类中成员应该有哪些?
首先肯定要一个套接字(其本质就是一个文件描述符),其次我们需要一个端口号,大家猜一下,我们需要一个IP地址吗?这个其实是不需要的,因为一款服务器/云服务器一般是不要指定某一个具体的IP地址的.
那我们bind的时候应该怎样传入参数呢?这个大家先不急,等会儿将代码写好了大家在回过来看就会清晰很多。为了方便使用我们还可以用一个包装器来包装我们将来要执行回调的函数。
4.2 🍎udpServer.hpp(重点)🍎
#pragma once
#include<iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include<functional>
using namespace std;
using fun_t =function<string(string)>;
class udpServer
{
public:
const static uint16_t defaultPort=8848;
udpServer(fun_t service=nullptr, uint16_t port =defaultPort)
:_service(service)
,_port(port)
{}
void init()
{
//1 创建套接字,打开网络文件
_socket=socket(AF_INET,SOCK_DGRAM,0);
if(_socket<0)
{
cerr<<"create socket fail"<<endl;
exit(-1);
}
//2 bind
sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_socket,(sockaddr*)&local,sizeof(local))<0)
{
cerr<<"bind fail"<<endl;
exit(-2);
}
cout<<"bind success"<<endl;
}
void start()
{
char buffer[1024];//自定义缓冲区
while(true)
{
//1 从客户端收消息
sockaddr_in client;//用作输出型参数,用来接受是哪个具体的客户端发送数据给服务端的
socklen_t len=sizeof(client);
int n=recvfrom(_socket,buffer,sizeof(buffer)-1,0,(sockaddr*)&client,&len);
if(n>0)
buffer[n]=0;
else
continue;
// cout<<"receive message success"<<endl;
string clientIp=inet_ntoa(client.sin_addr);
uint16_t clientPort=ntohs(client.sin_port);
cout<<clientIp<<"-"<<clientPort<<":"<<buffer<<endl;
//2 处理消息
string message=_service(buffer);
//3 发送消息给客户端
if(sendto(_socket,message.c_str(),message.size(),0,(sockaddr*)&client,sizeof(client))<0)
{
cerr<<"send message fail"<<endl;
exit(-3);
}
//cout<<"send message success"<<endl;
}
}
private:
int _socket;
uint32_t _port;
fun_t _service;
};
4.2.1 🍋注意事项🍋
- 1️⃣ 创建套接字所要的头文件是:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
但是sockaddr_in是定义在下面的头文件中的:
#include <netinet/in.h>
#include <arpa/inet.h>
所以我们写套接字编程的时候,这四个头文件都要带上。
-
2️⃣由于我们使用的是udp协议,所以我们使用的是
SOCK_DGRAM
,如果是tcp协议,我们使用的是SOCK_STREAM
。
至于第三个参数默认给0即可。 -
3️⃣在bind的时候我们由于类中成员并没有加上IP地址,所以我们使用下面这种写法:
4.3 🍎udpClient.cc🍎
#include"udpServer.hpp"
//./udpClient serverIp serverPort
void usage()
{
cout<<"Usage error\n\t"<<"serverIp serverPort"<<endl;
exit(-1);
}
int main(int argc,char*args [])
{
if(argc!=3)
{
usage();
}
string serverIp=args[1];
uint16_t serverPort=stoi(args[2]);
//1 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
cout<<"create socket fail"<<endl;
exit(-1);
}
//2 client要不要bind呢?要不要自己bind呢?
//要bind 但是不要自己bind 操作系统会帮助我们做这件事情
// 2 明确server
sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(serverPort);
server.sin_addr.s_addr=inet_addr(serverIp.c_str());
while(true)
{
//1 用户输入
string message;
cout<<"[grm]:";
getline(cin,message);
sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));
//2 接受服务端信息
char buffer[1024];
sockaddr_in tmp;
socklen_t len=sizeof(tmp);
int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(sockaddr*)&tmp,&len);
if(n>0)
{
buffer[n]=0;
cout<<buffer<<endl;
}
}
return 0;
}
4.3.1 🍋注意事项🍋
-
1️⃣在客户端这里,我们不难发现我们是没有自己手动bind的,为什么呢?
在这之前我们先要明确一点,就是客户端也是必须要bind的,这件事只不过是操作系统帮助我们做了。但是大家肯定又有一个疑问:为什么服务端我们要自己手动bind呀?
server的端口号要我们自己bind是因为服务器的端口号是众所周知的,且不能够随意改变;客户端不需要我们手动bind是因为害怕我们自己bind端口号时会发生冲突,所以这件事就交给了操作系统来帮助我们做。 -
2️⃣在明确服务端的时候我们使用了下面的接口函数:
这个函数有两个作用:
- 将字符串类型转化成四字节的uint32_t类型的四字节整数;
- 将主机序列转化成网络序列。
与这个函数具有同种功能的函数还有inet_aton
而上面的inet_ntoa
则是与inet_aton具有相反的功能。
除此之外,还有inet_pton
和inet_ntop
:
在这个系列的转换函数中不仅可以转换IPV4的地址,也可以转换IPV6的地址。
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?在APUE中, 明确提出inet_ntoa不是线程安全的函数;但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
4.4 🍎udpServer.c🍎
#include<memory>
#include"udpServer.hpp"
string dealMessage(const string& message)
{
return message;
}
void usage()
{
cout<<"Usage error\n\t"<<"serverPort"<<endl;
exit(0);
}
//./udpServer serverPort
int main(int argc,char* argv[])
{
if(argc!=2)
{
usage();
}
unique_ptr<udpServer> udpSer(new udpServer(dealMessage,8848));
udpSer->init();
udpSer->start();
return 0;
}
上述准备工作做好了后就可以来上手验证了:
注意我们在运行客户端的可执行程序时加上的IP地址可以直接是127.0.0.1
(表示本机),如果想要其他主机也能够正确访问的话要加上服务端的IP,也就是我们购买云服务器的公网IP地址。
4.5 🍎如何关闭防火墙+验证🍎
如果使用了云服务器的公网IP地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:
最后点击确认,就可以了,我们就发现列表中多出了两条:
到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:
这样我们就完成了一个简易版本的UDP网络通信的代码了。
除此之外,我们还可以实现一个客户端把命令给服务端,然后服务端在帮助我们执行:
static bool isPass(const std::string &command)
{
bool pass = true;
auto pos = command.find("rm");
if(pos != std::string::npos) pass=false;
pos = command.find("mv");
if(pos != std::string::npos) pass=false;
pos = command.find("while");
if(pos != std::string::npos) pass=false;
pos = command.find("kill");
if(pos != std::string::npos) pass=false;
return pass;
}
// 让客户端本地把命令给服务端,server再把结果给你!
// ls -a -l
std::string excuteCommand(std::string command) // command就是一个命名
{
// 1. 安全检查
if(!isPass(command)) return "you are bad man!";
// 2. 业务逻辑处理
FILE *fp = popen(command.c_str(), "r");
if(fp == nullptr) return "None";
// 3. 获取结果了
char line[1024];
std::string result;
while(fgets(line, sizeof(line), fp) != NULL)
{
result += line;
}
pclose(fp);
return result;
}
当我们运行时:
不难发现已经验证成功了。
上述代码中我们简单介绍下popen
函数:
这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)
当然在客户端和服务端中我们修改代码为生产者消费者模型(具体实现可以让一个线程读取消息,另外一个线程收消息)由于同一个文件描述符可以同时被多个线程读取,所以这样设计是OK的。这里我就不实验了,大家有兴趣可以自行下去尝试。
5 🍑简单的TCP网络程序 🍑
TCP的网络程序大致框架与UDP类似,其中不同点我会放在后面一点一点给出解释。
5.1 🍎tcpServer.hpp(重要)🍎
#pragma once
#include "err.hpp"
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include<signal.h>
using namespace std;
using func_t = function<string(const string &)>;
static const int backlog = 32;
class tcpServer
{
public:
tcpServer(func_t func, uint16_t port)
: _func(func), _port(port)
{
}
void init()
{
// 1 创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
cerr << "creat sock fail:" << strerror(errno) << endl;
exit(SOCK_ERR);
}
// 2 bind
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind fail" << endl;
exit(BIND_ERR);
}
// 3 listen
if (listen(_listensock, backlog) < 0)
{
cerr << "listen fail" << strerror(errno) << endl;
exit(LISTEN_ERR);
}
}
void service(int sock, const string &clientip, const uint16_t &clientport)
{
string who = clientip + "-" + std::to_string(clientport) + ":";
char buffer[1024];
while (true)
{
// 1 读取消息
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
// 2 处理消息
string message = _func(buffer);
cout << who << message << endl;
// server 发送消息给 client
int n = write(sock, message.c_str(), message.size());
if (n < 0)
{
cerr << "write fail" << strerror(errno) << endl;
exit(WRITE_ERR);
}
}
else if (n == 0)
{
cout << "client:" << clientip << "-" << to_string(clientport) << "quit,server also quit" << endl;
close(sock);
}
else
{
cerr << "read fail" << strerror(errno) << endl;
exit(READ_ERR);
}
}
}
void start()
{
while (true)
{
// 1 获取连接 明确是哪一个client发送来的
sockaddr_in client;
socklen_t len;
int sock = accept(_listensock, (sockaddr *)&client, &len);
if (sock < 0)
{
cerr << "accept fail" << strerror(errno) << endl;
continue;
}
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
cout << "get new link success:" << sock << " form " << _listensock << endl;
// 2 处理消息
service(sock, clientip, clientport);
private:
int _listensock;
uint16_t _port;
func_t _func;
};
5.1.1 🍋注意事项🍋
- 1️⃣由于是TCP,所以我们创建套接字时必须使用
SOCK_STREAM
. - 2️⃣由于TCP是保证可靠性的面向字节流的可靠协议,所以TCP在使用上肯定会比UDP复杂得多,会多上
listen
(监听) 和accept
(获取连接)。在linten
接口的创建中我们使用的第二个参数backlog
我们将放在后面再讲解,这里不太好解释。accept
接口的返回值也是一个套接字,这个套接字的任务是专门用来帮助我们读取和接受消息用的,而类中的_listensock
套接字的作用主要是进行前面套接字的创建和初始化工作。(可以简单的理解为_listensock
就相当于餐厅里在外面招呼客人的服务员,accept
接口的返回值套接字就是为客户真正意义上做饭的厨师) - 3️⃣我们将处理消息封装在了一个接口service中,在里面我们可以清晰得看见,读取消息用的是
read
,发送消息用的是write
,这正是我们学习文件操作时所用到得系统调用,这也很好的印证在LINUX下一切皆文件的思想。 - 4️⃣ 代码中所存在的错误都用了错误码来标识,错误码可参考下面:
enum
{
SOCK_ERR=1,
BIND_ERR,
USAGE_ERR,
LISTEN_ERR,
ACCEPT_ERR,
CONNECT_ERR,
WRITE_ERR,
READ_ERR,
};
tcpClient.cc:
#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "err.hpp"
using namespace std;
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
// 1 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "creat sock fail:" << strerror(errno) << endl;
exit(SOCK_ERR);
}
//2 client要bind,但是是不需要我们自己bind的
//client需要listen和accept吗?答案是不需要的
//3 connect
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_aton(serverip.c_str(), &(server.sin_addr));
int cnt = 5;
while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
sleep(1);
cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
if(cnt <= 0) break;
}
if(cnt <= 0)
{
cerr << "连接失败..." << endl;
exit(CONNECT_ERR);
}
char buffer[1024];
// 3. 连接成功
while(true)
{
string line;
cout << "Enter>>> ";
getline(cin, line);
write(sock, line.c_str(), line.size());
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout << "server echo >>>" << buffer << endl;
}
else if(s == 0)
{
cerr << "server quit" << endl;
break;
}
else
{
cerr << "read error: " << strerror(errno) << endl;
break;
}
}
close(sock);
return 0;
}
5.2 🍎tcpClient.cc🍎
#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "err.hpp"
using namespace std;
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
// 1 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "creat sock fail:" << strerror(errno) << endl;
exit(SOCK_ERR);
}
//2 client要bind,但是不需要我们自己bind的
//client需要listen和accept吗?答案是不需要的
//3 connect
string serverip=argv[1];
uint16_t serverport=stoi(argv[2]);
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_aton(serverip.c_str(), &(server.sin_addr));
int cnt = 5;
while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
sleep(1);
cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
if(cnt <= 0) break;
}
if(cnt <= 0)
{
cerr << "连接失败..." << endl;
exit(CONNECT_ERR);
}
char buffer[1024];
// 3. 连接成功
while(true)
{
string line;
cout << "Enter>>> ";
getline(cin, line);
write(sock, line.c_str(), line.size());
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
cout << "server echo >>>" << buffer << endl;
}
else if(s == 0)
{
cerr << "server quit" << endl;
break;
}
else
{
cerr << "read error: " << strerror(errno) << endl;
break;
}
}
close(sock);
return 0;
}
5.2.1 🍋注意事项🍋
- 1️⃣与UDP类似在bind的时候需要bind,但是这个工作不由我们自己完成,而是由OS来完成。
- 2️⃣在客户端是不用
listen
和accept
的,但是需要connect
(建立连接)我们可以自定义连接策略(失败了重连几次)。
5.3 🍎tcpServer.cc🍎
#include<memory>
#include"err.hpp"
#include"tcpServer.hpp"
string echoMssage(const string& message)
{
return message;
}
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " port\n"
<< std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=stoi(argv[1]);
unique_ptr<tcpServer> utcp(new tcpServer(echoMssage,port));
utcp->init();
utcp->start();
return 0;
}
5.4 🍎验证🍎
我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?
我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。
当我们把最先通信的客户端干掉之后:
消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。
来看看我们写的代码:
当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在service中我们是死循环的读取和发送消息的,那么当有另外的客户端请求时就不会给新的客户端建立连接,自然就发不出去,收不到喽!处理方式有两种:
- 多进程
- 多线程
5.4.1 🍋多进程🍋
void start()
{
while (true)
{
// 1 获取连接 明确是哪一个client发送来的
sockaddr_in client;
socklen_t len;
int sock = accept(_listensock, (sockaddr *)&client, &len);
if (sock < 0)
{
cerr << "accept fail" << strerror(errno) << endl;
continue;
}
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
cout << "get new link success:" << sock << " form " << _listensock << endl;
// 2 处理消息
//service(sock, clientip, clientport);
// 这样做当我们有多个client时会有什么问题?
// 方案一:多进程 让子进程帮助我们执行service
pid_t pid = fork();
if (pid < 0)
{
close(sock);
continue;
}
else if (pid == 0)
{
// child 建议关掉_listensock
close(_listensock);
service(sock, clientip, clientport);
exit(0);
}
// parent 一定要关闭sock,否则就会造成文件描述符的泄漏
close(sock);
waitpid(id, nullptr, WNOHANG);
if (ret == pid)
std::cout << "wait child " << pid << " success" << std::endl;
}
这样我们就能够很好的处理了。
除此之外还有一种更为精妙的方式:
我们可以再fork一下,当是父进程的时候就退出,执行到下面那肯定就是孙子进程,由OS领养,自然就不用关心回收状态了(OS会自动帮助我们回收)
当然,这还不是最好的方式,最好的方式我们可以使用下面的代码:
signal(SIGCHLD, SIG_IGN); // 推荐这样写
一行就搞定了,直接忽略掉子进程退出给父进程发送的消息。
5.4.2 🍋多线程🍋
// 方案二:多线程
pthread_t pid;
TcpData *pdata = new TcpData(sock, clientip, clientport, this);
pthread_create(&pid, nullptr, threadRoutine, pdata);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
TcpData* pd=static_cast<TcpData*>(args);
pd->_cur->service(pd->_sock,pd->_clientip,pd->_clientport);
}
其中TcpData类:
class tcpServer;
class TcpData
{
public:
TcpData(int sock, string &_clientip, uint16_t _clientport, tcpServer *cur)
: _sock(sock), _clientip(_clientip), _clientport(_clientport), _cur(cur)
{
}
int _sock;
string _clientip;
uint16_t _clientport;
tcpServer *_cur;
};
这样当我们再次运行时:
显然此时已经能够成功运行了。除了服务端使用多线程外,客户端也可以用一个线程池来创建,总的来说实现起来这里也不算太难,有兴趣的小伙伴可以参考博主之前实现的【Linux:线程池】来改装一下,有问题可以私信博主。
6 🍑TCP协议通讯流程🍑
这张图大家目前应该是看不太明白的,其实没啥关系,上面讲解的内容在博主后面的文章中会给出详细的解释,这里大家只需要简单的了解下过程就好了。
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来。
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手。
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手。
在学习socket API时要注意应用程序和TCP协议层是如何交互的?
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。