UDP协议:向应用程序提供一种面向无连接的服务 TCP协议:提供一种面向连接的,可靠的数据传输服务
网络通信程序编写时,到底传输层使用UDP好还是TCP?
TCP ---- 传输控制协议 ——面向连接,可靠传输,面向字节流 UDP ----用户数据报协议——面向无连接,不可靠传输,面向数据包 TCP保证可靠传输,但是传输速度没有UDP快。 TCP应用于安全性要求高 / UDP应用于实时性高的场景
socket套接字编程:网络程序的编写 udp通信编程: 通信的编程流程: 客户端:1.创建套接字 2.绑定地址信息(不推荐主动) 3.发送数据 4.接收数据 5.关闭套接字 服务端:1.创建套接字 2.绑定地址信息 3.接收数据 4.发送数据 5.关闭套接字
客户端不主动绑定端口地址,是为了降低端口冲突的概率,但是服务端为什么必须要绑定地址?
如果我们发送请求必须要知道服务端地址和端口信息 ——(如果不知道服务端地址和信息就无法发送请求)客户端是主动连接,而服务器是等待连接
scoket接口介绍 1.创建套接字
int socket(int domain, int type, int protocol) domain: 地址域——确定本次socket通信使用的版本 — AF_INET(ipv4网络协议) AF_INET6(ipv6网络协议) type: 套接字类型——流式套接字(SOCK_STREAM) / 数据报套接字(SOCK_DGRAM) protocol: 协议类型 (默认为0,流式默认TCP/数据报默认UDP) 返回值: 文件描述符 ——非负整数,套接字的操作句柄, 失败返回12.为套接字绑定地址信息
int bind(int sockfd, struct socketaddr* addr, socklen_t len); sockfd: 创建套接字返回的操作句柄 addr: 要绑定的地址信息 len: 要绑定的地址信息长度 struct sockaddr{ sa_family_t sa_family; char sa_data[4]; } struct sockaddr_in{ //IPV4 sa_family_t sa_family; //地址域 in_port sin_port; //端口号 stuct in_addr{ in addr_t addr} sin_addr; //IP地址信息 }bind可以绑定不同的地址结构,为了实现接口统一,因为用户定义地址结构的时候,定义自己需要的地址结构(例如 IPV4就使用struct sockaddr_in, IPV6就是用 struct sockaddr_in6) 但是进行绑定的时候,统一类型强转为sockaddr* 类型
3.接收数据
ssize_t recvfrom(int sockfd, char* buf, int len, int flag, stuct sockaddr* peer_addr, socklen_t* addrlen) sockfd: socket的操作句柄 buf: 一块缓冲区,用于接收从接收缓冲区中取出数据; len: 想要接收的长度 flag: 操作选项标识,默认为0,表示阻塞操作 pper_addr: 发送方地址长度 addlen: 想要获取的地址信息长度以及返回的实际长度 返回值: 成功返回实际接收到的数字字节长度, 失败返回-14.发送数据
ssize_t sendto(int sockfd, char* data, int len, int flag, struct sockaddr* peeraddr, socklen_t addrlen); sockfd: socket的操作句柄 data: 要发送数据的首地址 len: 要发送数据长度 flag: 默认为0,表示阻塞操作 peeraddr: 接收方的地址信息 addrlen: 地址信息长度5.关闭套接字
int close(int fd);网络字节序转换接口
uint16_t htons(uint16_t htonshort); //主机字节序到网络字节序的转换 uint32_t htons(uint32_t htonlong); uint16_t ntohs(uint16_t ntohshort); //网络字节序到主机字节序的转换 uint32_t ntohl(uint32_t ntohlong); in_addr_t inet_addr(const char* cp); //将字符串的点分十进制转换成网络字节序的整数IP地址 char* inet_ntoa(struct in_addr in); //将网络字节序的整数IP地址,转换为点分十进制IP地址 const char* inet_ntop(int af, const void* src, char* dst, socklen_t size); //将网络字节序的整数IP地址转换为字符串IP地址 int inet_pton(int af, const char* src, void* dst); //将字符串IP地址转换为网络字节序的IP地址使用c++封装一个UdpSocket类,实例化每个对象都是一个udp通信套接字,并且通过成员接口实现udp通信流程
// udpsocket.hpp #include<cstdio> #include<string> #include<unistd.h> #include<netinet/in.h> //包含地址结构信息 #include<sys/socket.h> //套接字接口信息 #include<arpa/inet.h> //字节序转换接口 class UdpSocket{ public: UdpSocket(int sockfd = -1):_sockfd(sockfd){} bool Socket(){//创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if(_sockfd < 0){ perror("socket error"); return false; } return true; } //为套接字绑定地址信息 bool Bind(const std::string& ip, uint16_t port){ struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); //htons将主机字节序短整型数据转换为网络字节序数据 addr.sin_addr.s_addr = inet_addr(ip.c_str()); //将字符串IP地址转换为网络字节序 socklen_t len = sizeof(struct sockaddr_in); //bind(操作句柄, 地址信息, 地址信息长度) int ret = bind(_sockfd, (struct sockaddr*)&addr, len); if(ret < 0){ perror("bind error"); return false; } return true; } //接收数据,获取发送端地址信息 bool Recv(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL){ //recv(套接字句柄, 接收缓冲区, 数据长度, 标志, 源端地址, 地址长度) struct sockaddr_in peer_addr; socklen_t len = sizeof(struct sockaddr_in); char tmp[4096] = {0}; int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len); if(ret < 0){ perror("recvfrom error!"); return false; } buf->assign(tmp, ret); //assign 从指定字符串中截取指定长度的数据到buf if(port != NULL){ *port = ntohs(peer_addr.sin_port); // 从网络字节序到主机字节序的转换 } if(ip != NULL){ *ip = inet_ntoa(peer_addr.sin_addr);//网络字节序到字符串IP地址转换 } return true; } //发送数据 bool Send(const std::string& data, const std::string& ip, const uint16_t port){ //sendto(套接字句柄, 数据首地址, 数据长度, 标志, 对端地址信息, 地址信息长度) struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len); if(ret < 0){ perror("send error!"); return false; } return true; } //关闭套接字 bool Close(){ if(_sockfd > 0){ close(_sockfd); _sockfd = -1; } return true; } private: int _sockfd; }; //udp_srv.cpp #include<iostream> #include<string> #include"udpsocket.hpp" #define CHECK_RET(q) if((q) == false){ return -1;} int main(int argc, char* argv[]){ //argc表示程序运行参数的个数 //./udp_src 192.168.x.x 8080 if(argc != 3){ std::cout << "Usage: ./udp_srv ip port\n" << std::endl; return -1; } uint16_t port = std::stoi(argv[2]); std::string ip = argv[1]; UdpSocket srv_sock; //创建套接字 CHECK_RET(srv_sock.Socket()); CHECK_RET(srv_sock.Bind(ip, port)); while(1){ std::string buf; std::string peer_ip; uint16_t peer_port; CHECK_RET(srv_sock.Recv(&buf, &peer_ip, &peer_port)); std::cout << "client[ "<< peer_ip << ":" << peer_port << "]" <<"say: " << buf << std::endl; buf.clear(); std::cout << "server say :"; std::cin >> buf; CHECK_RET(srv_sock.Send(buf, peer_ip, peer_port)); } CHECK_RET(srv_sock.Close()); } //udp_cli.cpp #include<iostream> #include<string> #include"udpsocket.hpp" #define CHECK_RET(q) if((q) == false){ return -1; } int main(int argc, char* argv[]){ //argc表示程序运行参数的个数 // // ./udp_src 192.168.x.x 8080 if(argc != 3){ std::cout << "Usage: ./udp_srv ip port" << std::endl; return -1; } uint16_t srv_port = std::stoi(argv[2]); std::string srv_ip = argv[1]; UdpSocket cli_sock; //创建套接字 CHECK_RET(cli_sock.Socket()); while(1){ std::cout << "client say:"; std::string buf; std::cin >> buf; CHECK_RET(cli_sock.Send(buf, srv_ip, srv_port)); buf.clear(); CHECK_RET(cli_sock.Recv(&buf)); std::cout << "server say:" << buf << std::endl; } CHECK_RET(cli_sock.Close()); }tcp通信编程: 1.创建套接字
int socket(int domain, int type, int protocol);2.绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t len);3.开始监听
int listen(int sockfd, int backlog); sockfd:将套接字设置为监听状态。并且监听状态后可以开始接收客户端连接请求 backlog:同一时间的并发连接数,决定同一时间最多可以接收多少个客户端请求4.获取新连接(从已完成连接队列中取出socket,并且返回这个socket的操作符句柄)
int accept(int sockfd, struct sockaddr* cli_addr, socklen_t len); sockfd:监听套接字,表示要获取哪个tcp服务端套接字建立连接 cli_addr:这个新的套接字对应的客户端地址信息 返回值: 新建套接字的描述符----外部程序中的操作句柄5.接收/发送数据(因为tcp通信套接字中已经标识了五元组,因此不需要接收数据的时候获取对方地址信息,发送数据的时候也不需要指定对方的地址信息)
ssize_t recv(int socket, char* buf, int len, int flag); ssize_t send(int socket, char*buf, int len, int flag);6.关闭套接字
int close(int sockfd);7.向服务端发起连接请求
int connect(int sockfd, struct sockaddr* srv_addr, int len); srv_addr: 服务端地址信息---给谁发送连接请求封装一个TcpSocket类,每一个实例化对象都是一个socket通信连接,通过
//tcpsocket.hpp #include<cstdio> #include<string> #include<unistd.h> #include<netinet/in.h> #include<arpa/inet.h> #include<sys/socket.h> #define MAX_LINSTEN 5 class TcpSocket{ public: TcpSocket():_sockfd(-1){} bool Socket(){ //socket(地址域, 套接字类型, 协议信息) _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(_sockfd < 0){ perror("socket error"); return false; } return true; } bool Bind(const std::string& ip, uint16_t port){ struct sockaddr_in addr; addr.family = AF_INET; addr.sin_port = htons(port); addr.sin_addrs.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = bind(_sockfd, struct sockaddr*(sockaddr_in), len); if(ret < 0){ perror("bind error"); return false; } return true; } bool Listen(int backlog = MAX_LISTEN){ //listen(套接字描述符, 最大并发数); int ret = listen(_sockfd, backlog); if(ret < 0){ perror("listen error"); return false; } return true; } bool Accept(TcpSocket* new_sock, std::string& ip = NULL, uint16_t* port = NULL){ //新建套接字描述符 = accpet(监听套接字描述符, 客户端地址信息, 地址信息长度) struct sockaddr_in addr; socklen_t len = sizeof(struct sockaddr_in); int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len); if(new_fd < 0){ perror("accpet error"); return false; } new_sock->sockfd = new_fd; if(ip != NULL){ (*ip) = ntohs(addr.sin_addr); } if(port != NULL){ *port = ntohs(addr.sin_port); } return true; } bool Recv(std::string* buf){ //recv(描述符, 缓冲区首地址, 接收数据长度, 标志位0--阻塞) char tmp[4096] = {0}; int ret = recv(_sockfd, tmp, 4096, 0); if(ret < 0){ perror("recv error"); return false; }else if(ret == 0){//recv默认阻塞,没有数就会等待,返回0,标识等待断开 printf("connection broken\n"); return false; } buf->assgin(tmp, ret); return true; } bool Send(const::string& data){ //send(描述符, 要发送的数据首地址, 数据长度, 标志位) int ret = send(_sockfd, data.c_str(), data.size(), 0); if(ret < 0){ perror("send error"); return false; } return true; } bool Close(){ if(_sockfd > 0){ close(_sockfd); _sockfd = -1; } return true; } bool Connect(const std::string& ip, uint16_t port){ //向服务端发起连接 //connect(描述符, 服务端地址信息, 地址信息长度) struct sockaddr_in addr; addr.family = AF_INET; addr.sin_port = port; addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = connect(_sockfd, (struct sockaddr*)addr, len); if(ret < 0){ perror("connect error"); return false; } return true; } private: int _sockfd; }; while (1) { TcpSocket new_sock; bool ret = lst_sock.Accept(&new_sock);//通过监听套接字获取新建连接 if (ret == false) { continue;//服务端不能因为一个新建套接字失败就退出 } std::string buf; new_sock.Recv(&buf);//通过新建连接与指定客户端进行通信 std::cout << "client say:" << buf << std::endl; buf.clear(); std::cout << "sever say:"; std::cin >> buf; new_sock.Send(buf); } lst_sock.Close();这样的话只能与每个客户端获取一次,但如果把获取新连接放在while循环外面的话,又只能与一个客户端进行通信。 当前在一个执行流中完成了多个操作:获取新连接,接收数据,发送数据。然而这些操作都有可能导致流程阻塞,因为我们在固定流程下,有可能对没有数据到来的socket进行操作。因此导致阻塞 将执行流分为2类: 1.获取新连接 一旦获取到一个新连接,就启动一个新的执行流,让这个新的执行流去与客户端进行通信 ①.因为没有新连接到来的阻塞,不会影响与客户端的通信 ②.与客户端通信的阻塞,不会影响获取新连接 2.与客户端进行通信
//tcp_srv.cpp #include<iostream> #include<signal.h> #include<sys/wait.h> #include"tcpsocket.hpp" void sigcb(int no) { //SIGCHILD是一个非可靠信号有可能丢失,因此在一次信号处理中就需要处理到没有子进程退出为止 while(waitpid(-1, NULL, WNOHANG) > 0); //返回值大于0表示有子进程退出 } int main(int argc, char* argv[]) { if (argc != 3) { std::cout << "usage: ./tcp_srv ip port\n"; return -1; } signal(SIGCHLD, sigcb); std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); TcpSocket lst_sock; CHECK_RET(lst_sock.Socket());//创建套接字 CHECK_RET(lst_sock.Bind(ip, port));//开始连接 CHECK_RET(lst_sock.Listen());//开始监听 while (1) { TcpSocket new_sock; bool ret = lst_sock.Accept(&new_sock);//通过监听套接字获取新建连接 if (ret == false) { continue;//服务端不能因为一个新建套接字失败就退出 } int pid = fork(); if (pid == 0) { while (1) { std::string buf; new_sock.Recv(&buf);//通过新建连接与指定客户端进行通信 std::cout << "client say:" << buf << std::endl; buf.clear(); std::cout << "sever say:"; std::cin >> buf; new_sock.Send(buf); } new_sock.Close(); exit(0); } new_sock.Close(); } lst_sock.Close(); } //tcp_cli.cpp #include<iostream> #include"tcpsocket.hpp" int main(int argc, char* argv[]) { if (argc != 3) { std::cout << "usage: ./tcp_cli ip port\n"; return -1; } std::string srv_ip = argv[1]; uint16_t srv_port = std::stoi(argv[2]); TcpSocket sock; CHECK_RET(sock.Socket());//创建套接字 CHECK_RET(sock.Connect(srv_ip, srv_port)); while (1) { std::string buf; std::cout << "client say:" << buf; std::cin >> buf; sock.Send(buf); buf.clear(); sock.Recv(&buf); std::cout << "server say:" << buf << std::endl; } sock.Close(); return 0; }