邯郸建立网站费用,中国进出口数据网,谈谈自己对市场营销的理解,小程序开发公司推选北京华网天下欢迎来到 破晓的历程的 博客 ⛺️不负时光#xff0c;不负己✈️ 文章目录 引言Udp和Tcp的异同相同点不同点总结 1.1、socket1.2、bind1.3、recvfrom1.4、sendto2.1、代码2.1、说明3.1、代码3.2、说明 引言
在前几篇博客中#xff0c;我们学习了Linux网络编程中的一些概念。… 欢迎来到 破晓的历程的 博客 ⛺️不负时光不负己✈️ 文章目录 引言Udp和Tcp的异同相同点不同点总结 1.1、socket1.2、bind1.3、recvfrom1.4、sendto2.1、代码2.1、说明3.1、代码3.2、说明 引言
在前几篇博客中我们学习了Linux网络编程中的一些概念。从本篇博客开始我们就正式开始写代码。本篇博客我们将写udp服务器和客户端代码并实现服务器和客户端通信。这些代码学习成本较高建议大家多敲几遍。如任何问题欢迎与我沟通。
Udp和Tcp的异同
UDP协议User Datagram Protocol用户数据报协议和TCP协议Transmission Control Protocol传输控制协议是计算机网络中两种常用的传输层协议它们在多个方面存在显著的异同。以下是对两者异同点的详细比较
相同点
层次位置两者都位于OSI模型的第四层——传输层为上层应用提供数据传输服务。作用都在网络通信中扮演着重要的角色用于在网络中的不同设备之间传输数据。
不同点
UDP协议TCP协议可靠性不提供可靠性保证不保证数据包的顺序、完整性和不重复。提供可靠的数据传输通过序列号、确认机制和重传机制确保数据的完整性和有序性。连接性无连接协议发送数据前不需要建立连接直接发送数据。面向连接的协议数据传输前需要建立连接通过“三次握手”机制确认连接状态。传输效率传输效率高因为不需要建立连接和维持连接状态开销小。传输效率相对较低因为需要建立和维护连接增加了额外的开销。实时性实时性较好适用于对实时性要求较高的应用如在线游戏、视频通话等。实时性较差因为需要等待连接建立和确认以及处理重传等机制。数据包大小数据包大小没有限制但通常受限于网络MTU最大传输单元。将数据分割成较小的数据块进行传输以适应不同的网络环境。拥塞控制不使用拥塞控制网络拥塞时不会降低发送速率。使用拥塞控制机制根据网络状况调整发送速率避免网络拥塞。应用场景适用于对可靠性要求不高但对实时性要求较高的场景如流媒体传输、DNS查询等。适用于对可靠性要求较高的场景如文件传输、网页浏览等。
总结
UDP协议和TCP协议在可靠性、连接性、传输效率、实时性、数据包大小和拥塞控制等方面存在显著的差异。选择哪种协议取决于具体的应用场景和需求。如果对数据传输的可靠性要求较高应选择TCP协议如果对实时性要求较高且可以容忍一定的数据丢失则可以选择UDP协议。在实际应用中两种协议经常结合使用以满足不同的网络需求。 不难发现Udp代码较简单,写起来相对的简单一些上手较容易。所以我们写使用Udp协议进行通信。 为了使大家更加容易理解。我们按照创建udp服务端的整个过程的先后顺序来进行讲解。最后写出完整的代码。
1.1、socket
网络通信必须要申请套接字。申请套接字对应的函数为socket。 #include sys/types.h /* See NOTES */#include sys/socket.hint socket(int domain, int type, int protocol);参数 ①domain domain协议域/协议族决定了socket的地址类型。常用的协议族有AF_INETIPv4、AF_INET6IPv6、AF_LOCAL或称AF_UNIXUnix域socket、AF_ROUTE等。在通信中必须采用与协议族对应的地址。例如AF_INET决定了要使用IPv4地址32位与端口号16位的组合。
②type typesocket类型指定了socket的类型。常用的socket类型有SOCK_STREAM流式套接字用于TCP、SOCK_DGRAM数据报套接字用于UDP、SOCK_RAW原始套接字允许对底层协议如IP或ICMP进行直接访问等。 ③protocol protocol协议通常情况下可以将其设置为0让系统自动选择type类型对应的默认协议。 返回值
当socket函数成功创建了一个套接字时它返回一个有效的套接字描述符socket descriptor。这个描述符是一个非负整数用于后续的网络操作如绑定、监听、连接、发送和接收数据等。如果在创建套接字时发生错误socket函数返回-1并设置全局变量errno以指示错误原因。此时可以调用errno变量或perror()函数来获取具体的错误信息。常见的错误码包括EACCES权限不足、EADDRINUSE地址已经被占用、EAFNOSUPPORT地址族不支持、EINVAL参数无效、EMFILE达到进程允许打开的最大文件数目、ENFILE系统打开文件数目过多、ENOBUFS/ENOMEM内存不足、EPROTONOSUPPORT协议不支持等。
1.2、bind
bind函数在网络编程中扮演着至关重要的角色它主要用于将一个本地协议地址包括IP地址和端口号赋予一个套接字。以下是关于bind函数的详细解释 #include sys/types.h /* See NOTES */#include sys/socket.hint bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);参数 ①sockfd这是由socket()函数返回的文件描述符代表已经创建的套接字。 ②addr这是一个指向特定协议地址结构的指针如struct sockaddr_in或struct sockaddr_un它包含了地址、端口和可能的IP地址信息。 ③addrlen这是地址结构的长度通常以字节为单位。对于IPv4通常使用sizeof(struct sockaddr_in)对于IPv6使用sizeof(struct sockaddr_in6)对于Unix域套接字使用sizeof(struct sockaddr_un)。 返回值
如果bind函数成功执行它返回0。如果出现错误返回-1并设置全局变量errno以指示错误原因。常见的错误包括EACCES权限不足、EADDRINUSE地址已经被使用、EADDRNOTAVAIL地址不可用、EAFNOSUPPORT地址族不支持该套接字类型、EINVAL套接字未打开、ENOTSOCK文件描述符不是套接字等。
使用场景
在TCP服务器程序中bind函数通常用于指定服务器应监听的端口号。服务器在启动时捆绑其众所周知的端口以便客户端可以连接到它。 对于UDP套接字bind函数同样用于指定接收数据的端口号。 在Unix域套接字中bind函数可以用来指定套接字在文件系统中的路径名。
注意事项
在调用bind函数之前套接字必须处于未连接状态对于面向连接的套接字如TCP。如果addr参数中的地址或端口号为0系统将为套接字自动选择一个可用的地址或端口号。在多线程环境中应确保对bind函数的调用是线程安全的避免竞态条件。绑定的本质将用户态的sockaddr_in设置进内核变为系统态。对于端口号而言如果用户没有调用bind函数进行显式绑定那么系统在第一次发送消息时会随机给套接字绑定一个端口号。
1.3、recvfrom
recvfrom函数是一个在POSIX兼容操作系统如Linux中用于接收数据的系统调用。它主要用于从指定的套接字接收数据并适用于面向无连接的协议如UDP用户数据报协议。 #include sys/types.h#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);参数 ①sockfd已经创建并绑定的套接字的文件描述符。 ②buf创建好的一块缓冲区的地址。用来承接从网络中读取到的数据。 ③len该块缓冲区的大小。 ④flags读取数据的方式。默认设为0——阻塞式读取。 ⑤src_addr输出型参数该结构体里面包含着数据发送方的信息如port、ip等等。如果不需要这些信息可以设为null。 ⑥‘’addrlen该结构体的大小。
返回值
成功时返回接收到的字符数字节数。如果没有可用数据或者连接已经关闭返回0。如果出现错误返回-1并设置errno错误号。此时可以通过perror()函数来打印出错误信息。
注意事项
在调用recvfrom函数之前需要先使用bind函数将socket绑定到一个地址上。如果套接字是非阻塞的recvfrom函数可能会在没有接收到任何数据时返回-1并设置errno为EAGAIN或EWOULDBLOCK。如果接收到的数据比缓冲区还大那么只会取缓冲区大小的数据并将剩余的数据丢弃。
1.4、sendto
sendto函数是一个系统调用用于将数据从指定的套接字发送到目标地址。它通常用于UDP用户数据报协议通信因为UDP是无连接的所以sendto函数允许你向一个特定的地址发送数据报而不需要事先建立连接。
#include sys/socket.h
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);参数
sockfd已经创建好的socket文件描述符。buf指向要发送的数据的缓冲区。len要发送的数据长度。flags发送选项标志可以是0或者像MSG_DONTWAIT这样的选项。MSG_DONTWAIT表示非阻塞发送如果发送缓冲区满则不等待直接返回。dest_addr目标地址的sockaddr结构体指针。对于IPv4这通常是一个指向struct sockaddr_in的指针对于IPv6则是一个指向struct sockaddr_in6的指针。addrlen目标地址结构体的长度例如sizeof(struct sockaddr_in)或sizeof(struct sockaddr_in6)。
返回值
sendto函数的返回值是一个long类型的整数表示发送的字节数。具体返回值有以下几种可能
如果返回值大于0则表示数据已经成功发送到了目标地址。返回值代表实际发送的字节数。如果返回值等于0表示发送的数据长度为0。这可能是因为buf指向的空间长度为0或者在使用UDP协议时sendto函数成功地发送了0字节的数据。如果返回值等于-1表示发送过程中出现了错误。此时可以通过检查errno的值来确定具体的错误原因。例如如果errno为EINTR表示sendto函数被一个信号中断了如果errno为EAGAIN或EWOULDBLOCK表示发送缓冲区已满无法立即发送数据这通常发生在使用了MSG_DONTWAIT标志的情况下。 需要注意的是sendto函数不保证数据的可靠传输。也就是说发送的数据可能会丢失或者接收方可能无法按照发送的顺序接收数据。如果需要可靠的数据传输应该使用TCP协议而不是UDP。 此外在使用sendto函数之前需要确保已经通过socket函数创建了一个套接字并且对于面向连接的套接字类型已经通过connect函数与目标地址建立了连接尽管对于UDP连接通常不是必需的但也可以通过connect建立默认的目标地址。同时也需要确保目标地址是有效的并且发送的数据缓冲区是正确设置的。 udp服务端 2.1、代码
#pragma once#include iostream
#include string
#include strings.h
#include cerrno
#include cstring
#include cstdlib
#include functional
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.hnamespace Server
{using namespace std;static const string defaultIp 0.0.0.0; //TODOstatic const int gnum 1024;enum {USAGE_ERR 1, SOCKET_ERR, BIND_ERR};typedef functionvoid (string,uint16_t,string) func_t;class udpServer{public:udpServer(const func_t cb, const uint16_t port, const string ip defaultIp):_callback(cb), _port(port), _ip(ip), _sockfd(-1){}void initServer(){// 1. 创建socket_sockfd socket(AF_INET, SOCK_DGRAM, 0);if(_sockfd -1){cerr socket error: errno : strerror(errno) endl;exit(SOCKET_ERR);}cout socket success: : _sockfd endl;// 2. 绑定portip(TODO)// 未来服务器要明确的port不能随意改变struct sockaddr_in local; // 定义了一个变量栈用户bzero(local, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port); // 你如果要给别人发消息你的port和ip要不要发送给对方local.sin_addr.s_addr inet_addr(_ip.c_str()); // 1. string-uint32_t 2. htonl(); - inet_addr//local.sin_addr.s_addr htonl(INADDR_ANY); // 任意地址bind服务器的真实写法int n bind(_sockfd, (struct sockaddr*)local, sizeof(local));if(n -1){cerr bind error: errno : strerror(errno) endl;exit(BIND_ERR);}// UDP Server 的预备工作完成}void start(){// 服务器的本质其实就是一个死循环char buffer[gnum];for(;;){// 读取数据struct sockaddr_in peer;socklen_t len sizeof(peer); //必填ssize_t s recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)peer, len);// 1. 数据是什么 2. 谁发的if(s 0){buffer[s] 0;string clientip inet_ntoa(peer.sin_addr); //1. 网络序列 2. int-点分十进制IPuint16_t clientport ntohs(peer.sin_port);string message buffer;cout clientip [ clientport ]# message endl;// 我们只把数据读上来就完了吗对数据做处理_callback(clientip, clientport, message);}}}~udpServer(){}private:uint16_t _port;string _ip; // 实际上一款网络服务器不建议指明一个IPint _sockfd;func_t _callback; //回调};
}2.1、说明
服务器一旦开始运行就不会停止。所以服务器本质就是一个死循环。这种一直运行的进程叫做常驻进程。一般来说服务器不会显式的绑定某一个ip。因为一个主机可能会有不同的ip。但是这台主机内的端口号是唯一的客户端都是发送信息到特定的端口号上。所以服务器为了可以接收到所有发到这台主机上的信息(不会存在数据丢弃的情况选择绑定0.0.0.0作为自己的ip。这样就可以接受到任何发送到这台主机指定端口的所有信息。 udp客户端 3.1、代码
#include iostream
#include string
#include strings.h
#include cerrno
#include cstring
#include cstdlib
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h
#include pthread.hnamespace Client
{using namespace std;class udpClient{public:udpClient(const string serverip, const uint16_t serverport): _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false){}void initClient(){// 创建socket_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd -1){cerr socket error: errno : strerror(errno) endl;exit(2);}cout socket success: : _sockfd endl;// 2. client要不要bind[必须要的]client要不要显示的bind需不需程序员自己bind不需要// 写服务器的是一家公司写client是无数家公司 -- 由OS自动形成端口进行bind-- OS在什么时候如何bind}static void *readMessage(void *args){int sockfd *(static_castint *(args));pthread_detach(pthread_self());while (true){char buffer[1024];struct sockaddr_in temp;socklen_t temp_len sizeof(temp);size_t n recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)temp, temp_len);if (n 0)buffer[n] 0;cout buffer endl;}return nullptr;}void run(){pthread_create(_reader, nullptr, readMessage, (void *)_sockfd);struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(_serverip.c_str());server.sin_port htons(_serverport);string message;char cmdline[1024];while (!_quit){//cerr # ; // ls -a -l// cin message;fprintf(stderr, Enter# );fflush(stderr);fgets(cmdline, sizeof(cmdline), stdin);cmdline[strlen(cmdline)-1] 0;message cmdline;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)server, sizeof(server));}}~udpClient(){}private:int _sockfd;string _serverip;uint16_t _serverport;bool _quit;pthread_t _reader;};
} // namespace Client3.2、说明
客户端需要绑定端口号吗客户端需要显式的绑定端口号吗
端口号是需要绑定端口号的但是不需要显式的绑定端口号的。绑定端口号的工作交给操作系统自主完成这个工作由操作系统在客户端初次发送消息时完成。
相对于服务端来说客户端必须绑定特定的端口号但是端口号的数值对于客户端来说就显得不太重要。
服务端必须指定特定的端口号以供客户端根据该端口号来向服务端发送消息。但是客户端而言如果显式指明端口号必然会出现两个客户端竞争一个端口号的情况。所以在通信时就由操作系统随机分配一个端口号供客户端进行通信。