网站美化的目标,广东工程建设咨询有限公司网站,制作一个网站需要多少钱,途牛网站建设系列文章目录 文章目录 系列文章目录一、服务端实现1.1 创建套接字socket1.2 指定网络接口并bind2.3 设置监听状态listen2.4 获取新链接accept2.5 接收数据并处理#xff08;服务#xff09;2.6 整体代码 二、客户端实现2.1 创建套接字socket2.2 指定网络接口2.3 发起链接con…系列文章目录 文章目录 系列文章目录一、服务端实现1.1 创建套接字socket1.2 指定网络接口并bind2.3 设置监听状态listen2.4 获取新链接accept2.5 接收数据并处理服务2.6 整体代码 二、客户端实现2.1 创建套接字socket2.2 指定网络接口2.3 发起链接connect2.4 发送数据并接收2.5 整体代码2.6 绑定问题 三、问题与改进3.1 问题描述3.2 解决方法3.2.1 问题版本3.2.2 多进程版3.4.3 进程池暂未实现3.4.4 多线程版3.4.5 线程池版 四、TCP与UDP的对比 一、服务端实现
1.1 创建套接字socket
和上篇文章UDP的使用一致创建套接字
调用系统接口socket函数帮助我们创建套接字本质是把文件和网卡关联起来
参数介绍 domain一个域标识了这个套接字的通信类型网络或者本地 只用关注上面三个类第一个与第二个AF_UNIX/AF_LOCAL表示本地通信而AF_INET表示网络通信 type套接字提供服务的类型 我们用UDP实现所以使用SOCK_DGRAM protocol想使用的协议默认为0即可因为前面的两个参数决定了就已经决定了是TCP还是UDP协议了 返回值 成功则返回打开的文件描述符指向网卡文件其实就是文件描述符失败返回-1 创建套接字的本质其实就是创建了一个文件描述符并返回该文件描述符的值 只是该文件描述符是用于对应服务的网路数据传输 // 1. 创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){lg.LogMessage(Fatal, socket error, sockfd : %d\n, _listensock);exit(1);}lg.LogMessage(Info, socket success, sockfd : %d\n, _listensock);1.2 指定网络接口并bind
和上篇文章UDP的使用一致服务端需要手动bind 参数介绍 socket创建套接字的返回值 address通用结构体上一章Linux网络——网络套接字有详细介绍 address_len传入结构体的长度 我们要先定义一个sockaddr_in结构体将结构体内对应的字段填充好再将结构体作为参数传递
struct sockaddr_in {short int sin_family; // 地址族一般为AF_INET或PF_INETunsigned short int sin_port; // 端口号网络字节序struct in_addr sin_addr; // IP地址unsigned char sin_zero[8]; // 用于填充使sizeof(sockaddr_in)等于16
};创建结构体后要先清空数据初始化我们可以用memset也可以用系统接口
#include strings.hvoid bzero(void *s, size_t n);填充端口号的时候要注意端口号是两个字节的数据涉及到大小端问题 在计算机中的普遍规定在网络中传输的数据都是大端的 所以为了统一无论我们机器是大端还是小端在调用接口的时候都将IP与端口号从主机序列转化为网路序列
端口号的接口
#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);IP的接口 对于IP其实有两步首先将字符串转换为整型再解决大小端问题 系统给了直接能解决这两个问题的接口
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.hint inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);// 点分十进制字符串in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in);struct in_addr inet_makeaddr(int net, int host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);这里的inet_addr就是把一个点分十进制的字符串转化成整数再进行大小端处理
代码 // 2. 指定网络接口并bindstruct 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;int n bind(_listensock, (struct sockaddr *)local, sizeof(local));if (n ! 0){lg.LogMessage(Fatal, bind error\n);exit(2);}lg.LogMessage(Debug, bind socket success, sockfd: %d\n, _listensock);2.3 设置监听状态listen
这里TCP跟UDP有所不同
要把socket套接字的状态设置为listen状态只有这样才能一直获取新链接接收新的链接请求
举个例子 我们买东西如果出现了问题会去找客服如果客服不在那么就回复不了所以规定了客服在工作的时候必须要时刻接收回复消息这个客服所处的状态就叫做监听状态 关于第二个参数backlog后边讲TCP协议的时候介绍目前先直接用 const static int default_backlog 1;// 3. 设置socket为监听状态int m listen(_listensock, default_backlog);if (m ! 0){lg.LogMessage(Fatal, listen error\n);exit(3);}lg.LogMessage(Debug, listen socket success, sockfd: %d\n, _listensock);做完这些初始化工作就完成了总代码 void Init(){// 1. 创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){lg.LogMessage(Fatal, socket error, sockfd : %d\n, _listensock);exit(1);}lg.LogMessage(Info, socket success, sockfd : %d\n, _listensock);// 2. 指定网络接口并bindstruct 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;int n bind(_listensock, (struct sockaddr *)local, sizeof(local));if (n ! 0){lg.LogMessage(Fatal, bind error\n);exit(2);}lg.LogMessage(Debug, bind socket success, sockfd: %d\n, _listensock);// 3. 设置socket为监听状态int m listen(_listensock, default_backlog);if (m ! 0){lg.LogMessage(Fatal, listen error\n);exit(3);}lg.LogMessage(Debug, listen socket success, sockfd: %d\n, _listensock);}
2.4 获取新链接accept
上面初始化完毕现在开始就是要运行服务端而TCP不能直接发数据因为它是面向链接的必须要先建立链接。
参数介绍 sockfd文件描述符找到套接字 addr输入输出型参数是一个结构体用来获取客户端的信息 addrlen输入输出型参数客户端传过来的结构体大小 返回值 成功返回一个文件描述符 失败返回-1 而我们知道sockfd本来就是一个文件描述符那么这个返回的文件描述符是什么呢 举个例子 我们去吃饭的时候会发现每个店铺门口都会有人来招揽顾客这个人把我们领进去门店后然后他就会继续站在门口继续招揽顾客而我们会有里面的服务员来招待我们给我们提供服务 这里的揽客的人就是_listensock而服务员就是返回值的文件描述符 意思就是_listensock的作用就是把链接从底层获取上来返回值的作用就是跟客户端通信 从这里就知道了成员变量中的_listensock并不是通信用的套接字而是专门用来获取链接的套接字 void Start(){_is_running true;while (_is_running){// 4. 获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int sockfd accept(_listensock, (struct sockaddr *)peer, len);if (sockfd 0){lg.LogMessage(Fatal, socket accept error);continue;}lg.LogMessage(Debug, accept socket success, get a new sockfd: %d\n, sockfd);Service(sockfd);close(sockfd);}}2.5 接收数据并处理服务
当客户访问服务器的时候必定是想要完成某件事并且得到某件事完成的结果我们称这个过程为服务 服务端收到客户端发来的信息或请求后进行分析判断完成客户端想要完成的任务并返回给客户端
我们这里写一个简单的运用此处的服务就是读取发来的信息并发回给客户端
void Service(int sockfd){char buffer[1024];while (true){int n read(sockfd, buffer, sizeof(buffer) - 1);if (n 0){buffer[n] 0;std::cout client say# 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);break;}}}
2.6 整体代码
#pragma once
#include iostream
#include string
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include string.h
#include Log.hpp
#include nocopy.hppstatic const int default_fd -1;
const static int default_backlog 1;class TcpServer : public nocopy
{
public:TcpServer(const uint16_t port): _port(port), _listensock(default_fd), _is_running(false){}void Init(){// 1. 创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock 0){lg.LogMessage(Fatal, socket error, sockfd : %d\n, _listensock);exit(1);}lg.LogMessage(Info, socket success, sockfd : %d\n, _listensock);// 2. 指定网络接口并bindstruct 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;int n bind(_listensock, (struct sockaddr *)local, sizeof(local));if (n ! 0){lg.LogMessage(Fatal, bind error\n);exit(2);}lg.LogMessage(Debug, bind socket success, sockfd: %d\n, _listensock);// 3. 设置socket为监听状态int m listen(_listensock, default_backlog);if (m ! 0){lg.LogMessage(Fatal, listen error\n);exit(3);}lg.LogMessage(Debug, listen socket success, sockfd: %d\n, _listensock);}void Service(int sockfd){char buffer[1024];while (true){int n read(sockfd, buffer, sizeof(buffer) - 1);if (n 0){buffer[n] 0;std::cout client say# 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);break;}}}void Start(){_is_running true;while (_is_running){// 4. 获取连接struct sockaddr_in peer;socklen_t len sizeof(peer);int sockfd accept(_listensock, (struct sockaddr *)peer, len);if (sockfd 0){lg.LogMessage(Fatal, socket accept error);continue;}lg.LogMessage(Debug, accept socket success, get a new sockfd: %d\n, sockfd);Service(sockfd);close(sockfd);}}~TcpServer(){}private:uint16_t _port;int _listensock;bool _is_running;
};#include TcpServer.hpp
#include memoryvoid Usage(const std::string s)
{std::cout Usagr: s local_port std::endl;
}int main(int argc,char *argv[])
{if (argc ! 2){Usage(argv[0]);return 1;}std::unique_ptrTcpServer tcpser std::make_uniqueTcpServer(std::stoi(argv[1]));tcpser-Init();tcpser-Start();return 0;
}二、客户端实现
2.1 创建套接字socket // 1. 创建socketint _sockfd socket(AF_INET, SOCK_STREAM, 0);if (_sockfd 0){lg.LogMessage(Fatal, socket error, sockfd : %d\n, _sockfd);exit(1);}lg.LogMessage(Info, socket success, sockfd : %d\n, _sockfd);2.2 指定网络接口 // 2. 指定网络接口struct sockaddr_in send;memset(send, 0, sizeof(send));send.sin_family AF_INET;send.sin_port htons(stoi(argv[2]));send.sin_addr.s_addr inet_addr(argv[1]);2.3 发起链接connect 参数说明 这里的addr和addrlen填入的是服务端信息 在UDP通信中客户端在sendto的时候会自动绑定IP和portTCP这里就是在connect的时候绑定
// 3. 进行连接int n connect(_sockfd, (struct sockaddr *)send, sizeof(send));2.4 发送数据并接收 while (true){std::string inbuffer;std::cout Please Enter# ;getline(cin, inbuffer);int n write(_sockfd, inbuffer.c_str(), inbuffer.size());if (n 0){char buffer[1024];int m read(_sockfd, buffer, sizeof(buffer) - 1);if (m 0){buffer[m] 0;cout get a echo messsge - buffer endl;}else if (m 0 || m 0){break;}}else{break;}}2.5 整体代码
#include iostream
#include string
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include string.h
#include Log.hppvoid usage(std::string s)
{std::cout Usagr: s server_ip server_port std::endl;
}int main(int argc, char *argv[])
{if (argc ! 3){usage(argv[0]);return 0;}// 1. 创建socketint _sockfd socket(AF_INET, SOCK_STREAM, 0);if (_sockfd 0){lg.LogMessage(Fatal, socket error, sockfd : %d\n, _sockfd);exit(1);}lg.LogMessage(Info, socket success, sockfd : %d\n, _sockfd);// 2. 指定网络接口struct sockaddr_in send;memset(send, 0, sizeof(send));send.sin_family AF_INET;send.sin_port htons(stoi(argv[2]));send.sin_addr.s_addr inet_addr(argv[1]);// 3. 进行连接int n connect(_sockfd, (struct sockaddr *)send, sizeof(send));while (true){std::string inbuffer;std::cout Please Enter# ;getline(cin, inbuffer);int n write(_sockfd, inbuffer.c_str(), inbuffer.size());if (n 0){char buffer[1024];int m read(_sockfd, buffer, sizeof(buffer) - 1);if (m 0){buffer[m] 0;cout get a echo messsge - buffer endl;}else if (m 0 || m 0){break;}}else{break;}}return 0;
}2.6 绑定问题
首先bind的作用是允许应用程序指定一个端口号用于监听传入的数据报或数据流
对于服务端 需要绑定一个公开的端口号允许大家访问如果是随机的其他人不知道也就访问不了所以服务端需要绑定
对于客户端 客户端在给服务器发信息的同时服务器也可能给客户端发信息所以客户端为了监听服务器有没有给自己回信息也需要bind一个端口号用于监听但客户端不需要显式的bind因为客户端的端口号不会被所有人访问别人不需要知道所以OS自动帮我们bind一个随机的端口号另外也是为了防止端口号重复避免破坏唯一性
那么为什么前面服务端必须显示的绑定port呢 因为服务器的端口号是众所周知的不能改变如果变了就找不到服务器了 而客户端只需要有就可以只用标识唯一性即可 举个例子 我们手机上有很多的app而每个服务端是一家公司写的但是客户端却是多个公司写的 如果我们绑定了特定的端口万一两个公司都用了同一个端口号呢这样就直接冲突了 OS会自动填充主机IP和随机生成端口号进行绑定在发送数据的时候自动绑定 所以创建客户端我们只用创建套接字即可
三、问题与改进
3.1 问题描述 上述图片是服务端的代码由于整个服务端都是单进程的所以这意味着在同一时间只能有一个客户端的链接能被accept其他客户端的信息是接受不到的因为一但某个客户端被成功accept了那么单进程就会走到Service中Service是一个死循环服务所以只要客户端不退出服务端是无法accept到其他链接的
3.2 解决方法
3.2.1 问题版本 3.2.2 多进程版
使用多进程主进程进行accept子进程进行Service服务
引入新问题 主进程需要阻塞等待子进程退出还是accept不了新连接
两个解决办法
用孙子进程执行服务 pid_t pid fork();if (pid 0){lg.LogMessage(Fatal, fork error);close(sockfd);continue;}else if (pid 0){close(_listensock);if (fork() 0){Service(sockfd);close(sockfd);exit(0);}exit(0);}close(sockfd);waitpid(pid, nullptr, 0);自定义信号让父进程忽略子进程结束时发送给父进程的信号 signal(SIGCHLD,SIG_IGN);pid_t pid fork();if (pid 0){lg.LogMessage(Fatal, fork error);close(sockfd);continue;}else if(pid 0){close(_listensock);Service(sockfd);close(sockfd);exit(0);}close(sockfd);
3.4.3 进程池暂未实现
存在问题 如果先创建子进程备用子进程拿不到主进程accept的sockfd
3.4.4 多线程版
篇幅较长Gitee连接多线程版
3.4.5 线程池版
篇幅较长Gitee连接线程池版
四、TCP与UDP的对比
对比UDP服务器TCP服务器多了获取新链接和监听的操作 因为UDP是不可靠传输而TCP是是可靠传输所以TCP在传输数据之前会进行连接的建立
UDP和TCP的区别
对于TCP协议有几个特点 传输层协议有连接正式通信前要先建立连接可靠传输在内部帮我们做可靠传输工作面向字节流 对于UDP协议有几个特点 传输层协议无连接不可靠传输面向数据报 注意 这里的可靠与不可靠并不是贬义词而是中性词可靠或者不可靠形容的是特点而不是优劣 并不是说可靠传输就好用而是要区分应用场景和需求