客户评价网站建设,施工企业岗前培训内容记录,可以自己买个服务器做网站吗,贵阳做网站公司排名其实有了UDP的基础#xff0c;TCP不管怎么说学习起来都还是比较舒服的#xff0c;至少是比直接就学习TCP的感觉好。
这篇文章最多就是介绍一下起手式#xff0c;如果想带业务的话和UDP那篇是完全一样的#xff0c;就不进行演示了。 总的来说还是很简单的。 目录 Echo服务端…其实有了UDP的基础TCP不管怎么说学习起来都还是比较舒服的至少是比直接就学习TCP的感觉好。
这篇文章最多就是介绍一下起手式如果想带业务的话和UDP那篇是完全一样的就不进行演示了。 总的来说还是很简单的。 目录 Echo服务端起手式服务端LOOP客户端起手客户端LOOP验证改进方案一方案二 验证 Echo
我们还是从最简单的不带业务的Echo开始。
服务端起手式
服务器起手式 首先要说明的一点是TCP是面向字节流有连接。 而UDP创建好套接字后不管连接直接recvfromsendto就可以发送。
在TCP编码中会体现出有连接的特点面向字节流会在理论中提及代码中实践。 创建套接字 首先与UDP不同的是UDP的第二个参数是SOCK_DGRAM。
_listenfd ::socket(AF_INET, SOCK_STREAM, 0);关于这两个参数不同含义更详细的放在下图中了。 另外这次使用listenfd接收以前我们获得的就是sockfd直接使用这个描述符进行收发那么叫listenfd肯定是有特别用意。 进行bind bind没什么好说的仍旧是要注意填参问题。
int n ::bind(_listenfd, (struct sockaddr *)local, len);进入listen状态 从这里就可以看到面向连接的痕迹了。 那么什么叫listen状态 我们举一个小例子 UDP就是无人售货店不许用老板看店即可买东西。 但是TCP就是有人售货店需要老板在才可以买东西而TCP就是那个有人售货店listen就是设置为老板模式这样就可以接客了。
第一个参数就是你得到的listenfd。 第二个参数是连接队列长度这个值我们暂时先不用管一般默认设置为4 8 16即可不需要太大。
n ::listen(_listenfd, gbacklog);服务端LOOP
accept 来了新的连接我们就要去accept了 换句话说就是在有人售货店中现在如果来客人了老板就需要去接客。
int sockfd ::accept(_listenfd, (sockaddr *)peer, len);参数是输出型参数来一个连接就可以获得这个连接的基本信息。 但是要注意填参时一定要正确填参虽然addrlen是一个输出型参数但仍然需要正确初始化不能不初始化。 我就犯了这个错误导致有时错误accept或者connect错误connect在客户端会提到甚至搞的我以为这是tcp特性… 但是我们注意到返回值是一个文件描述符那么他是什么与listenfd有何区别我们举一个例子进行理解 上图可以形象的帮助理解返回值。 其中饭店 服务器 客户 新连接 揽客员 listenfdsocket返回值 服务员 sockfd accept返回值
listenfd不直接提供服务sockfd才直接提供服务 所以在TCP服务端我们一般将socket返回值叫做监听套接字。
没有连接时就会陷入阻塞状态accept失败返回-1这里可以理解为揽客员都把路人拉到店里面但是路人又临时有事离开了导致拉客失败~。 下段代码是TCP的框架代码与UDP有很大的不同。
void Loop()
{_isrunning true;while (_isrunning){sockaddr_in peer;socklen_t len sizeof(peer);int sockfd ::accept(_listenfd, (sockaddr *)peer, len);if (sockfd 0){continue;}// version 0 提供长服务。service(sockfd);}_isrunning false;
}与UDP有很大的不同 每一次循环都对应着一个新连接我们随后要对这个新连接进行长服务 而UDP是不管来的是谁都统一处理。 读写操作 我们在长服务中进行读写操作。
因为我们是面向字节流而管道、文件也都是流。 且Linux下一切皆文件管道、文件都是文件所以都是可以用文件操作readwrite进行读写所以这里也可以使用文件操作
void service(int sockfd)
{// 长服务while (true){char inbuffer[1024];int n read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n 0){inbuffer[n] 0;int m write(sockfd, inbuffer, n);if (m 0){break;}}else if (n 0){break;}else{break;}}::close(sockfd);
}但是这里有一个细节要注意read的返回值0是表示读到的字节数无\00时表示读到文件结尾在这里表示客户端结束。 就想吃在管道那里写端管了读端读出的自然都是0了。
客户端起手
socket
int sockfd ::socket(AF_INET, SOCK_STREAM, 0);注意客户端仍旧是用socket的返回值进行通信。 connect 同样客户端还是不用bind在UDP那是sendto时OS进行绑定而在这就是connect时帮我们自动绑定。
sockaddr_in peer;
memset(peer, 0, sizeof(peer));
inet_pton(AF_INET, ip.c_str(), peer.sin_addr);
peer.sin_port htons(port);
peer.sin_family AF_INET;
int n ::connect(sockfd, (sockaddr *)peer, sizeof(peer));客户端LOOP
这里没啥好说的就是普通的write read。 但是要注意由于是面向字节流所以这里的处理是有问题的具体如何操作请看用户自定义协议与序列化
while (true)
{std::cout Please enter#;std::string line;getline(std::cin, line);n write(sockfd, line.c_str(), line.size());if (n 0){break;}char inbuffer[1024];n read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n 0){inbuffer[n] 0;std::cout inbuffer std::endl; }
}完整代码链接
验证 但是当我们多开一个客户端就会发现一个服务端只能服务一个客户端。 原因在于我们当前是串行必须等当前客户端退出才能accept下一个客户端并没有处理并发客户端的能力。
所以改进就来了
改进
方案一
多进程方案。 我们知道子进程是会继承父进程的代码和数据的同样的文件描述符表也会继承因此我们就可以利用这个特点让子进程去执行代码和数据。 首先我们fork之后就要将不需要的文件描述符关闭 对于子进程来说是怕误操作 对于父进程来说是为了防止文件描述符泄露因为进程具有独立性当子进程继承了后就与父进程相互不影响了。若是父进程不关闭一直申请fd却步关闭那么就造成了资源泄露。
但是注意由于父进程要进行等待所以此时仍然是串行。 想要解决有两种方法
由于子进程结束时会发送SIGCHLD信号于是进行信号忽略signal(SIGCHLD, SIG_IGN)。另外这也是最佳方案。下段代码所示:我们进行再次fork并让子进程退出所以此时孙子进程成为了孤儿进程孤儿进程的父进程是OS结束归OS管完美的利用了OS的特性。
pid_t pid fork();
if (pid 0)
{if (fork() 0) exit(0);// 子进程关闭不需要的fd防止误操作。::close(_listenfd);service(sockfd, inetaddr);exit(0);
}
waitpid(pid, nullptr, 0);
// 父进程关闭不需要的fd防止内存泄漏。
::close(sockfd);方案二
多线程 我们创建一个新线程让新线程去执行service即可。
验证
此时同时启动两个客户端也可以完美的进行并发了。