朋友们、伙计们,我们又见面了,本期来给大家带来socket编程相关的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. TCP通信
2. 整体代码架构
3. Server端基础框架
3.1 Init接口
3.2 Start接口
3.2.1 提供服务
版本①
版本②
版本③
版本④
版本⑤
4. Client端基础框架
4.1 建立连接
4.2 进行通信
4.2.1 断线重连
5. 匹配线程池的客户端
6. 总结
1. TCP通信
TCP是面向连接的,客户端和服务端要进行通信时,需要先建立连接;
一般情况下是client主动建立连接;
所以server就因该一直等待连接的到来。
2. 整体代码架构
为了更加直观,我们采用分模块编写:
- Comm.hpp:用来保存一些错误码
- Log.hpp:日志系统
- nocopy.hpp:禁止服务器进行拷贝
- Makefile:自动构建代码
- Main.cc:启动服务端
- TcpClient.cc:实现TCP客户端
- TcpServer.hpp:实现TCP服务端
首先我们先把Comm.hpp所需要用到的错误码进行设置,其中至于每种错误码有什么用途在后面写代码的时候会体现出来,先来添加进去:
然后就将我们之前实现的日志系统引入进来:
Log.hpp:
#pragma once#include <iostream> #include <fstream> #include <string> #include <cstdarg> #include <ctime> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>enum {Debug = 0,Info,Warning,Error,Fatal };enum {Screen = 10,OneFile,ClassFile };std::string LevelToString(int level) {switch (level){case Debug:return "Debug";case Info:return "Info";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "Unknown";} }const int defaultstyle = Screen; const std::string default_filename = "log."; const std::string logdir = "log";class Log { public:Log() : style(defaultstyle), filename(default_filename){mkdir(logdir.c_str(), 0775);}void Enable(int sty) //{style = sty;}std::string TimeStampExLocalTime(){time_t currtime = time(nullptr);struct tm *curr = localtime(&currtime);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,curr->tm_hour, curr->tm_min, curr->tm_sec);return time_buffer;}void WriteLogToOneFile(const std::string &logname, const std::string &message){umask(0);int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if (fd < 0)return;write(fd, message.c_str(), message.size());close(fd);// std::ofstream out(logname);// if (!out.is_open())// return;// out.write(message.c_str(), message.size());// out.close();}void WriteLogToClassFile(const std::string &levelstr, const std::string &message){std::string logname = logdir;logname += "/";logname += filename;logname += levelstr;WriteLogToOneFile(logname, message);}void WriteLog(const std::string &levelstr, const std::string &message){switch (style){case Screen:std::cout << message;break;case OneFile:WriteLogToClassFile("all", message);break;case ClassFile:WriteLogToClassFile(levelstr, message);break;default:break;}}void LogMessage(int level, const char *format, ...) // 类C的一个日志接口{char leftbuffer[1024];std::string levelstr = LevelToString(level);std::string currtime = TimeStampExLocalTime();std::string idstr = std::to_string(getpid());char rightbuffer[1024];va_list args; // char *, void *va_start(args, format);// args 指向了可变参数部分vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);va_end(args); // args = nullptr;snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",levelstr.c_str(), currtime.c_str(), idstr.c_str());std::string loginfo = leftbuffer;loginfo += rightbuffer;WriteLog(levelstr, loginfo);}// void operator()(int level, const char *format, ...)// {// LogMessage(int level, const char *format, ...)// }~Log() {}private:int style;std::string filename; };Log lg;class Conf { public:Conf(){lg.Enable(Screen);}~Conf(){} };Conf conf;
为了防止服务器进行拷贝,所以我们实现一个禁止拷贝的模块,然后在编写TCP服务器时直接对其继承即可:
顺便也将我们的Makefile文件进行实现:
关于TCP通信的一些接口的使用在后面我们会边用边讲;
3. Server端基础框架
未来我们要启动我们的服务器,首先需要输入一个端口用于绑定,所以通过命令行参数的方式通过用户输入来进行获取,获取成功之后,服务端肯定需要有一个初始化的过程(创建套接字、绑定、监听等),初始化结束后服务器便可以进行正常启动;
Main.cc:
所以在实现客户端时,需要设置的成员变量肯定需要有端口号于套接字,实现的方法一个初始化功能和一个开始的功能;后面有需要再进行补充
TcpServer.hpp:
3.1 Init接口
我们对服务器初始化时需要做的事情:
- ① 创建TCP套接字;
- ② 填充本地网络信息
- ③ 绑定
- ④ 监听(TCP特有的)
接下来我们来一步一步实现:
① 创建TCP套接字
我们创建套接字的接口是socket:
最后一个参数根据我们前面两个参数自己推导;
② 填充本地网络信息并绑定
我们要进行网络通信,就需要将我们本地网络信息(port、ip)填充在一个公共的结构体中,为了后面的绑定操作;关于网络我们需要引入的头文件:
绑定我们需要用到的接口是bind:
这里为了方便我们将类型转化设置为宏;
③ 设置监听状态
因为我们前面说到过,进行TCP通信的时候,客户端和服务端是需要建立连接的,client主动建立连接,所以server就应该等待连接的到来,所以将服务端设置为监听状态就是TCP通信特有的状态;
我们设置监听状态的接口为listen:
对我们的目前写好的代码进行编译运行:
可以看到是可以正常运行的。
3.2 Start接口
我们应该都知道,服务器一旦启动就一定是一个死循环,这点是毋庸置疑的,所以,我们可以设置一个bool类型的标志位,用来表示服务器是否启动;我们在构造函数那里直接先将标志为设为false;在Start接口中将标志位改为true,这样就表示服务器启动了;
因为TCP通信中,client是主动发起连接的,所以服务器要启动,就需要先获取连接;所以我们需要用到获取连接的接口:accept
这个函数需要重点来了解一下;
首先先来看一下他的返回值:
失败返回-1,但是一旦获取成功就会获得一个新的套接字(文件描述符),那么他的返回值该怎么理解呢?
就比如一家饭店,饭店外面有专门拉客的服务员叫张三,专门从大街上招揽客人,饭店一天的客流量取决于他,饭店里面也有服务员叫李四,专门服务进店吃饭的客人,张三从外面拉的客并不是由张三服务他们进店用餐,而是在店里的李四来负责,所以accept函数的第一个参数sockfd就类似于张三(也就是我们从开始一直到现在使用的_sock),他的返回值就类似于李四,用于我们获取新连接,进行通信的。
对他的返回值理解之后, 他其他的两个参数都是主导的输出型参数,用于保存新连接用于通信的网络数据,所以接下来我们需要对我们之前的_sock进行一下修改,他其实真真的名称应该是listensock,accept的返回才是用于通信的sock,接下来用代码来实现:
注意:需要将之前用到_sock的地方全部改成_listensock;
3.2.1 提供服务
我们的客户端获取连接成功之后,就需要提供服务了,提供服务的方式有很多种,我们想实现4个版本的提供服务的接口,为了代码不冗余,我们将提供服务单独设置一个名为Service的接口,调用完提供服务的接口之后需要将对应的sockfd关闭。
版本①
我们之前也发现了,其实创建套接字本质上就是分配一个文件描述符,那么既然是文件描述符,那么我们就可以通过文件系统的接口来进行读取和写入,所以我们version1版本就是用read和write来实现的;
我们既然要通过文件来读取,势必要传递sockfd;
这里需要注意的是我们通过文件来进行读取和写入时,就类似于之前学到的管道!
建议版本①写完先不要写后面的版本,先直接去写client端代码,先将整个TCP通信运行起来;
版本②
我们的版本①实现的是单进程版的,同时只能由一个进程进行访问,所以我们版本②就实现多进程版的,同时可以有多个进程进行访问;
- 多进程版这里实现就需要创建子进程,防止子进程僵尸,父进程需要等待子进程;
- 如果我们想要子进程和父进程并发的执行,因为父进程等待子进程可以选择阻塞和非阻塞,非阻塞等待面临一个如果短时间获取的连接很多,后面就不会有新连接到来,也就不会执行等待,如果是阻塞等待,那么父进程还必须等待子进程运行完,其实还是一个单进程的方式;
- 所以这里我们在子进程中再创建孙子进程,让孙子进程去获取连接,即使子进程退出,孙子进程也会被系统领养,所以这样子就达到了并发执行的效果;
- 因为子进程会拷贝父进程的文件描述符表,为了防止文件描述符泄露,所以父子进程需要关闭不需要的文件描述符。
有了多进程版本大家可以自己测试一下,多起几个ssh,然后同时访问服务器,同时发消息也是可以的。
版本③
版本②的多进程代码是通过孙子进程进行获取连接的,如果不用孙子进程连接,并且父进程还不想等待子进程呢?我们还可以通过信号的方式让子进程在程序结束后自动回收资源,这个问题在之前的信号的处理章节中对SIGCHLD信号进行详细的说明了,我们对该信号进行SIG_IGN即可:
版本④
版本③实现的是一个信号版的多进程,但是进程创建的成本比较高,所以我们直接使用多线程来进行通信;
- 使用多线程需要注意的是,我们主线程不想要等待新线程,那么要将新线程设置为分离状态;
- 我们的新线程要进行获取连接,那么就需要传递文件描述符,那么为了更加安全的将文件描述符传递给新线程,所以我们需要单独设置一个类用来进行文件描述符的获取,以及获取连接的方法Service;
- 在这里我们还可以将具体的client端信息打印出来,所以我们需要单独设计一个hpp文件,专门用来处理client端具体信息。
这里我们需要单独来了解一个接口:inet_ntoa
该函数的功能就是将网络字节序ip转化为字符串风格ip地址,但是需要注意的是该接口在多线程时是不安全的,因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
为了线程安全起见,我们需要使用到的接口是inet_ntop:
这个函数也是将网络ip转为字符串ip,这个函数相比前面的函数不同的是:这个函数在转化的时候需要我们自己维护一块缓存区,也就意味着在多线程的时候,每一个线程都自己都会拥有一块空间,互不干扰;
InetAddr.hpp:
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>class InetAddr { public:InetAddr(struct sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port);// _ip = inet_ntoa(_addr.sin_addr); //char *inet_ntoa(struct in_addr in),返回的是字符串地址,那么字符串在哪里?char ipbuffer[64];inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer)); // 1. 网络转本机 2. 4字节ip,字符串风格的ip_ip = ipbuffer;}std::string Ip() { return _ip; }uint16_t Port() { return _port; };std::string PrintDebug(){std::string info = _ip;info += ":";info += std::to_string(_port); // "127.0.0.1:4444"return info;}const struct sockaddr_in &GetAddr(){return _addr;}bool operator==(const InetAddr &addr){// other codereturn this->_ip == addr._ip && this->_port == addr._port;}~InetAddr() {}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr; };
复合代码:
#pragma once#include <iostream> #include <string> #include <cerrno> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #include <pthread.h>#include "Log.hpp" #include "nocopy.hpp" #include "Comm.hpp" #include "InetAddr.hpp"class TcpServer;#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr) const static int default_backlog = 5; // TODOclass ThreadData { public:ThreadData(int sock, TcpServer *svr, struct sockaddr_in &peer): sockfd(sock), svr_ptr(svr), addr(peer){}int SockFd() { return sockfd; }TcpServer *GetServer() { return svr_ptr; };~ThreadData(){close(sockfd);}private:int sockfd;TcpServer *svr_ptr;public:InetAddr addr; };class TcpServer : public nocopy { public:TcpServer(uint16_t port) : _port(port), _isrunning(false){}~TcpServer(){}void Init(){// 1. 创建套接字 本质是文件 file fd_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){// 创建套接字失败lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 填充本地网络信息struct sockaddr_in local;local.sin_family = AF_INET; // 进行网络通信local.sin_port = htons(_port); // 主机转网络的portlocal.sin_addr.s_addr = INADDR_ANY; // 绑定任意地址// 3. 绑定if (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 4. 设置socket为监听状态,TCP特有的if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}void Service(int sockfd, InetAddr addr){char buffer[1024];// 一直进行IOwhile (true){int n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// 读取成功buffer[n] = 0;std::cout << addr.PrintDebug() << "# " << buffer << std::endl;// 回显std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0) // 在网络接口中使用read返回为0,表示读到了文件结尾(对端已经关闭了连接){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}static void *HandlerRequest(void *args){// 设置分离pthread_detach(pthread_self());//ThreadData *td = static_cast<ThreadData *>(args);td->GetServer()->Service(td->SockFd(), td->addr);delete td;return nullptr;}void Start(){signal(SIGCHLD, SIG_IGN); // 如果对SIG_IGN进行忽略,子进程退出的时候,自动释放自己的资源_isrunning = true;while (_isrunning){// 5. 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0){lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue; // 一次获取不成功就一直获取}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);// 6. 提供服务// v1// v2 多进程(孙子进程处理)// v3 多进程(基于信号)// v4 多线程ThreadData *td = new ThreadData(sockfd, this, peer);pthread_t tid;pthread_create(&tid, nullptr, HandlerRequest, td);// 主线程和新线程,不需要关闭对应的文件描述符,只需要将线程设置为分离}}private:uint16_t _port; // 端口号int _listensock; // listen套接字bool _isrunning; // 服务是否启动// TODO };
版本⑤
版本④实现了多线程版本,但是还是存在一个缺陷问题,当连接来了才进行创建线程,那么我们想要做到的是,提前就准备了一批线程,当连接来了的时候,直接使用这批线程,提前创建好了可以拿来直接使用,所以就需要用到线程池版本;(关于线程池的实现在之前的博客中实现过:https://blog.csdn.net/Yikefore/article/details/146205689?spm=1001.2014.3001.5501)
将线程池引入之后的代码就不展示了,源码我会放在文章最后面,这里主要说一下思路;
- 我们将线程池引入之后,我们想实现一个基于TCP的网络服务,所以需要一个任务列表;
- 所以我们需要在Init中对线程池进行启动;
- 启动之后的线程池还没有任务,所以我们需要将我们需要执行的任务进行绑定,然后push到任务列表中;
- 绑定的任务中实现的是根据用户输入从而使用指定的服务;
- 然后我们获取用户的输入服务类型,从而执行对应的服务;
- 在启动服务器的时候构建具体的服务;
大致思路是这样子,具体的代码在文章末尾;
4. Client端基础框架
未来我们想要启动我们的client端需要指明服务器的ip和port,所以通过命令行参数的方式对server_ip和server_port进行获取;
要进行TCP通信,必须要做的事情就是先创建套接字,填充本地网络信息,绑定,这些写法和写服务端的写法一模一样,这里就直接实现了;
需要注意的是,在填写本地网络信息时对于主机转网络的接口很多,选择适合自己的就好!
Client.cc:
4.1 建立连接
关于client的绑定问题在udp那里已经说过了,client是需要绑定的,但是不需要我们显示的进行绑定,在TCP这里,client在发起连接时,如果连接成功了,OS就会自动进行本地绑定,所以我们直接建立连接即可。
建立连接需要用到的接口是connect:
连接失败我们目前先设置为直接返回,后面我们还想在连接失败时设置一些其他的功能,后面再实现。
需要注意的是,client并不像server那样在连接成功时会产生新的sockfd,后面我们就用connect成功连接的sockfd用来通信即可。
4.2 进行通信
前面在server端的提供服务的接口那里提到过,sockfd本质上是文件描述符,所以我们可以通过文件读写的方式来进行通信,所以先来用read、write的方式来实现一下:
#include "TcpServer.hpp" #include "Comm.hpp" #include <memory>void Usage(std::string proc) {std::cout << "Usage : \n\t" << proc << " local_port\n"<< std::endl; }int main(int argc, char *argv[]) {if(argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = atoi(argv[1]); // 获取端口号std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);tsvr->Init();tsvr->Start();return 0; }
代码测试:
上面实现的代码只是单进程的,后面我们会优化出多进程的。
4.2.1 断线重连
当服务器关闭的时候,我们的客户端会连接失败,我么想要给我他设置一个5s的重连时间,为了方便,我们将连接服务器的步骤封装起来,并且在每次重连的时候都从5s开始重连,其实这个工作也可以不用做,只是为了了解一下服务器重连的简单思路,这里就直接展示代码了;
#include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr) #define Retry_Count 5 // 重连次数void Usage(const std::string &process) {std::cout << "Usage: " << process << " server_ip server_port" << std::endl; }bool AccessService(std::string &serverip, uint16_t &serverport, int *cnt) {bool ret = true;ssize_t m = 0;ssize_t n = 0;std::string inbuffer;// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return false;}// 2. 填充本地网络信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 字符串ip转化为网络序列4字节ip// 3. 主动建立连接n = connect(sockfd, CONV(&server), sizeof(server)); // 自动进行bindif (n < 0){std::cerr << "connect error" << std::endl;ret = false;goto END;// TODO}*cnt = 1;// 4. 进行通信while (true){std::cout << "Please Enter# ";std::getline(std::cin, inbuffer);if (inbuffer == "quit")return true;n = write(sockfd, inbuffer.c_str(), inbuffer.size());if (n > 0){char buffer[1024];m = read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;std::cout << "get a echo message -> " << buffer << std::endl;}else if (m == 0){break;}else{ret = false;goto END;}}else{ret = false;goto END;}} END:close(sockfd);return ret; }// ./tcp_client server_ip server_port int main(int argc, char *argv[]) {if (argc != 3){Usage(argv[0]);return 1;}// 获取server的IP和portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int cnt = 1;while (cnt <= Retry_Count){bool result = AccessService(serverip, serverport, &cnt);if (result){// 连接并访问成功break;}else{// 访问失败进行重连sleep(1);std::cout << "server offline, retrying..., count : " << cnt << std::endl;cnt++;}}if (cnt >= Retry_Count){std::cout << "server offline" << std::endl;}return 0; }
上面实现的是一个简单的客户端代码,这个代码可以配合Main.cc与服务端的前四个版本进行测试;
5. 匹配线程池的客户端
我们客户端这里需要改变的时,将之前的长服务改成了短服务;
#include <iostream> #include <string> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr) #define Retry_Count 5 // 重连次数void Usage(const std::string &process) {std::cout << "Usage: " << process << " server_ip server_port" << std::endl; }bool AccessService(std::string &serverip, uint16_t &serverport, int *cnt) {bool ret = true;ssize_t m = 0;ssize_t n = 0;std::string inbuffer;char service_list[1024];// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return false;}// 2. 填充本地网络信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 字符串ip转化为网络序列4字节ip// 3. 主动建立连接n = connect(sockfd, CONV(&server), sizeof(server)); // 自动进行bindif (n < 0){std::cerr << "connect error" << std::endl;ret = false;goto END;// TODO}*cnt = 1;// 读取服务列表m = read(sockfd, service_list, sizeof(service_list) - 1);if (m > 0){service_list[m] = 0;std::cout << "服务器提供的服务列表是: " << service_list << std::endl;}// 4. 进行通信std::cout << "请你选择服务# ";std::getline(std::cin, inbuffer);write(sockfd, inbuffer.c_str(), inbuffer.size());std::cout << "Enter> ";std::getline(std::cin, inbuffer);if (inbuffer == "quit")return true;n = write(sockfd, inbuffer.c_str(), inbuffer.size());if (n > 0){char buffer[1024];m = read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else if (m == 0){return true;}else{ret = false;goto END;}}else{ret = false;goto END;}END:close(sockfd);return ret; }// ./tcp_client server_ip server_port int main(int argc, char *argv[]) {if (argc != 3){Usage(argv[0]);return 1;}// 获取server的IP和portstd::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int cnt = 1;while (cnt <= Retry_Count){bool result = AccessService(serverip, serverport, &cnt);if (result){// 连接并访问成功break;}else{// 访问失败进行重连sleep(1);std::cout << "server offline, retrying..., count : " << cnt << std::endl;cnt++;}}if (cnt >= Retry_Count){std::cout << "server offline" << std::endl;}return 0; }
测试结果:
源码:https://gitee.com/yue-sir-bit/linux/tree/master/4.tcp_server_echo
6. 总结
基于TCP协议的客户端/服务器程序的一般流程:
TCP要进行通信,需要先建立连接,而建立连接需要进行三次握手;
- 其本质就是先建立共识;
- 所以在connect和accept时都会阻塞,都是在等待三次握手的成功;
- TCP是面向连接的,在不想通信的时候,需要断开连接,此时就需要四次挥手;
- TCP是全双工通信的,所以在断开连接时,双方都需要断开,所以就是四次挥手;
- 三次握手和四次挥手是通信双方OS底层自动做的工作,不要用户去实现。
- 关于三次挥手和四次握手的具体在后面会详细介绍!
- 我们在发送请求消息的时候需要从本地转网络,但是在接受读取的时候直接可以用read/write来直接读和直接写,这是因为在read/wirte内部已经进行了转网络序列,所以可以直接使用;
- 我们之前实现的udp是面向数据报,数据和数据之前有边界的,读写是对等的,发送了一次数据那么我也只能读一次数据;
- 但是tcp是面向字节流的,读写是不对等的,发送与接受与对方无关,写端写了好几次数据,读端有可能一次性读完,有可能分好几次,就类似于之前的管道,写端多次写入了好几个报文,但是读端一次性全部读取了,用户拿到的这一堆数据就需要用户自己处理,至于这些数据怎么用完全由用户来制定,这就是面向字节流;
- 所以在tcp代码中,我们进行网络IO的时候,使用的read和write其实是有bug的,要能正确的读取,是要根据用户自定义协议来完成的,就比如:我们向文件中写入单词就直接写入,但是想要从文件中读取单词就比较费劲了,要么读取一整行,将读取出来的字符串根据空格进行分割,拿到完整的单词,要么就一个字符一个字符的读取,读到空格时那么前面的就是一个完整的单词,在这个过程中,按照空格来区分完整单词就是我们用户和文件之前自定义的一种协议,所以文件本身也是面向字节流的;
- 后面了解完自定义协议在重新设计;