Linux--套接字编程
一、前言1.套接字编程1做什么编写网络通信程序实现两个主机上进程之间的数据传输。网络通信也是进程间通信方式的一种相较于以前的进程间通信可以实现跨主机之前的所学习的进程间通信方式-管道共享内存等是在同一个主机上不同进程间进行进程间通信。2网络通信方式也是进程间通信方式的一种但是通信两端有不同的角色客户端通常指的是部署在客户机上发起业务请求的一端。服务端通常指的是部署在服务提供商的服务器上向客户端提供服务器。3网络通信也分为两种不同的架构:1. C/S:客户端服务器模式client/server通常程序员既要实现服务器端程序 也要实现客户端程序谁安装了客户端 谁才能与服务器通信2.B/S:浏览器服务器模式browser/server通常程序员只需要只需要服务端程序客户端不需要实现客户端就是浏览器通俗的来说客户端就是客户需要使用的程序软件服务器就是开发供应商所提供的在C/S架构下客户需要使用什么程序就需要安装下载这个软件所以也就是程序员需要书写客户端程序同时需要书写服务端程序。而B/S架构下只需要程序员书写服务端程序的原因是每台计算机本身就自带有浏览器所以在使用浏览器这个软件的时候就不需要再编写这个程序让客户安装下载计算机本身就已经存在。我们学习的网络编程叫做套接字编程socket编程学习内容1.基于socket套接字相关的接口实现网络通信客户端与服务器程序2. Socket套接字一套网络通信API接口3.通信程序的编写流程-主要关心的是传输层通信程序的编写4. 传输层两个典型协议TCPUDP二、正式学习Linux下一切皆是文件所有操作都是文件操作包括网络了通信。Socket API 接口创建套接字:在内核中创建网络通信描述信息并获取操作句柄-文件描述符句柄就是文件描述符创建套接字核心其实就是套接字的文件描述符 内核中创建socket结构体变量。Intsocketint domain,int type,int protocol① domain:地址域 描述是什么样的通信 也就是 通信地址类型比如AF_UNIX域间通信本地通信struct sockaddr_un不跨主机 在同一主机之间进行通信AF_INETIPV4通信struct sockaddr_InAF_INET6:IPV6通信 struct sockaddr_In6② type:套接字类型比如SOCKET_STREAM流式套接字 有序的 可靠的 双向的 基于字节流SOCKET_DGRAM:数据报套接字 无连接 不可靠 有最大的长度限制SOCKET_RAW提供更底层的原始的数据包捕获③ protocol:协议类型 选择要使用的协议默认值0SOCKET_STREAM是TCP协议 SOCKET_DGRAM是UDP协议IPPROTO_TCP:TCP协议 IPPROTO_UDPUDP协议返回值成功 则返回操作语柄套接字描述符 失败返回12.为套接字绑定地址信息设置发送数据时的源端地址信息当前设备网卡IP地址端口号端口号0~1024是预留端口需要管理员权限告知操作系统网卡收到那些数据应该交给当前套接字进行处理告诉网卡发送数据的时候数据的源端地址信息是什么接收数据的时候 哪些数据应该交给我来处理Int bind(int sockefd,struct sockaddr* addr,socklen_t len)sockfd套接字描述符 socket返回的描述符addr:地址信息sockaddr*是通用的地址信息struct sockaddr_un struct sockaddr_In6struct sockaddr_In{Unshort sin_family;// 地址域类型Unshort sin_port;//要绑定的端口号Struct in_addr{uint32 s_adrr;}sin_addr;要绑定的ip}len地址信息长度防止访问越界33.连接服务器——》只用于TCP客户端Int connect(int sockefd,struct sockaddr* addr,socklen_t len)Addr:要连接的服务器地址信息返回值成功返回0 失败返回-14.开始监听用于TCP协议的服务端目的告诉操作系统可以开始接受客户端的连接请求了。Int listen(int sockfd,int backlog);Backlog同一时间能够接受的最大客户端连接请求数量。限制了同一时间最多有多少客户端能够连接服务器。避免网络攻击导致资源耗尽。返回值成功返回0 失败返回-1。5.获取就绪连接用于TCP协议的服务端作用获取与指定客户端进行通信的操作句柄描述符Int accept (int sockfd,struct sockaddr*peer_addr,socklen_t*len);peer_addr获取指定对端地址信息这里对端就是客户端len既要设置获取多长的地址并且还要返回实际的地址信息长度函数的参数分为三类输入型参数 输出型参数 输入输出型参数返回值成功返回0 失败返回-16.接收数据接收数据的接口是一个阻塞接口recv的时候没有数据则等待分为两类TCP协议的接口ssize_t recv(int sockfd,void* buf,size_t len,int flag)-》其实就是干了这样一件事找到对应的套接字的描述符从这个套接字的缓冲区将数据接收过来。在此之前客户端要发送数据到服务器网卡上一开始就会创建一个套接字这个套接字就会有一个完整的五元组和一个接收缓冲区一个发送缓冲区在客户端发送数据的时候给这个数据也有完整的五元组描述了数据从哪来到哪去其中数据的dip和网卡对应上才能将这个数据发送到对应网卡上去通过dport来定位这个数据应该交给哪一个通信程序或者套接字如果数据中保存的dport和一个socket中的sport对应那么这个数据就是要交给这个socket来处理的。之后就是recv的过程。在接收数据后就应该从缓冲区里面删除掉。Sockfd:对于客户端来说就是socket的返回值对于服务端来说就是accept的返回值Flag:标志位比如MSG_DONTWAIT设置非阻塞等待 如果本次recv能拿到数据则则直接返回没有接收到数据就报错返回。MSG_PEEK:获取接收数据 但是不从缓冲区删除数据。常用于一种探测型接收。返回值成功则返回实际读取到的数据长度 、出错则返回-1 、TCP通信断开 返回0。UDP协议的接口sszize_t recvfrom(int sockfd,void* buf,size_t len,int flag,struct sockaddr*peer_addr,socklen_t addrlen)peer_addr:输出参数 返回对端地址信息 表示该数据是谁发送过来的返回值成功则返回实际读取到的数据长度 出错则返回-1 不会返回0 因为他是数据报是无连接的。7.发送数据发送数据相关的接口也是阻塞接口发送过快 数据将socket发送缓冲区满了就会阻塞Ssize_t send(int sockfd,char*data,size_t len,int flag);Ssize_sento(int sockfd,char*data,size_t len,int flag,struct sockaddr*toaddr,socklen_t addrlen);Toaddr描述数据要发送给谁返回值实际要发送的数据长度 出错则返回-18.关闭套接字int closeint fd;字节序转换接口字节序转换针对的数据类型 short int long float double 无符号两种1. 从主机字节序转换为网络字节序2字节数据转换Uint16_t htons(uint16_t val);4字节数据的转换uint32_t htonl(uint32_t val);注意短整型数据转换不能使用 htonl例如假设数据在是0X01 02主机字节序固定小端存储低地址存低位高地址存高位所以内存存放就是02 01网络字节序固定大端存储低地址存高位高地址存低位。所以在内存存储的时候就会是01 02进行存放但是C语言有一个操作叫做隐式类型转换会将除了long long 以外的其他数据看成4个字节的数据根据对应的数据类型截取对应的字节大小。2.从网络字节序转换为主机字节序2字节数据转换Uint16_t ntohs(uint16_t val);4字节数据的转换uint32_t ntohl(uint32_t val);IP地址的特殊性本质就是4字节的无符号整数 :ntohl , htonl,进行转换即可但是IP为了能够可以更加便于记忆 会采用点分十进字符串进行表示:192.168.12.12所以很多是时候 IP地址的转换 并不是给于实际的数字 而是一个字符串Struct in_addr inet_addrconst char*ipstr;将字符串点分十进制IP地址 直接转换为网络字节序数字IP地址Const char*inet_ntoa(struct in_addr addr);将网络字节序数字IP地址 转换为字符串点分十进制IP地址Inet_ntoa这个接口是非线程安全的 因为会造成交叉写入Const char*inet_ntop(int domain,void* src,char*dst,size_t size)功能将网络字节序整数IP地址准换为字符串IP地址Domain:AF_NET,UDP通信程序的编写流程服务端创建套接字为套接字绑定地址接收数据 recvfrom(fd,buf,sz,flag,*addr,len);向客户端响应数据sendto(data,*addr)关闭套接字客户端创建套接字绑定套接字地址-》通常客户端并不主动绑定地址发送数据 sendto(data,*addr)-》隐含操作检测套接字是否已经绑定地址 如果未绑定则从闲置端口池获取一个闲置的顿口IP进行绑定接收响应recvfrom关闭套接字代码展示/*编写一个简单的UDP通信服务端程序功能模拟简单的聊天程序1.接收客户端进行展示。2.从标准输入获取一个信息响应给客户端。*/编写过程需要用到的头文件服务端和客户端一样#includestdio.h #includestring.h #includeunistd.h//close #includesys/socket.h//socket api接口 #includenetinet/in.h//地址结构和协议宏声明 #includearpa/inet.h//字节序转换的api接口服务端程序int main(){ //1.创建套接字 //int socket(int domain,int type,int protocol) //AF_INET:IPV4通信 SOCK_DGRAM数据报套接子 int sockfdsocket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); if(sockfd0){ perror(创建套接字失败); return -1; } //1.5构造地址信息结构填充要绑定的地址信息 绑定的ip地址必须是当前已有的网卡ip地址绑定那个网卡则接收 struct sockaddr_in addr;//定义IPV4的地址结构 //向这个地址进行赋值 addr.sin_familyAF_INET; addr.sin_porthtons(9090);//要转换为网络字节序 //in_addr_t inet_addr(const char*cp) //int inet_pton(int af,char *cp,void*buf) addr.sin_addr.s_addrinet_addr(192.168.14.128); inet_pton(AF_INET,192.168.14.128,addr.sin_addr); //2.为套接子绑定地址信息(,. //int bind(int fd, const struct sockaddr *addr, socklen_t len) socklen_t addrlensizeof(struct sockaddr_in); int retbind(sockfd,(struct sockaddr*)addr,addrlen); if(ret0){ perror(绑定地址信息失败); return -1; } while(1){ //3.接受客户端请求 recvfrom 不仅仅接收数据 还接受发送端地址 //ssize_t recvfrom(fd, *buf, n, flags, *addr, *addr_len) struct sockaddr_in peerAddr; socklen_t addrlensizeof(struct sockaddr_in); char buffer[1024]{0};//用于存放获取出来的数据 retrecvfrom(sockfd,buffer,1023,0,(struct sockaddr*)peerAddr,addrlen); if(ret0){ perror(recvfrom接受数据失败); continue; } printf(客户端[%s:%d]say:%s\n, inet_ntoa(peerAddr.sin_addr),ntohs(peerAddr.sin_port),buffer); //4.捕获键盘输入 memse(buffer,0x00,1024); printf(服务端 say:); fflush(stdout); scanf(%s,buffer); //5.向客户端响应数据 //ssize_t sendto(fd, *data, n, flags,*dst_addr, addr_len) retsendto(sockfd,buffer,strlen(buffer),0,(struct sockaddr*)peerAddr,addrlen); if(ret0){ perror(sendto 发送数据失败); continue; } } //关闭套接子 close(sockfd); return 0; }客户端程序#includestdio.h #includestring.h #includeunistd.h//close #includesys/socket.h//socket api接口 #includenetinet/in.h//地址结构和协议宏声明 #includearpa/inet.h//字节序转换的api接口 #define CHECK_RESULT(ret,msg)if((ret)0){peeror(msg);return -1;} int main(){ //1.创建套接子 int sockfdsocket(AF_INET,SOCK_DGRAM,0); CHECK_RESULT(sockfd,创建套接子失败); //2.为套接字绑定地址信息 一般客户端不主动绑定 //3.捕获键盘输入 char buffer[1024]{0}; printf(客户端 say:); fflush(stdout); scanf(%s,buffer); //4.发送数据 struct sockaddr_in srvAddr; srvAddr.sin_familyAF_INET; srvAddr.sin_porthtons(9090); srvAddr.sin_addr.s_addr(192.168.14.128); socklen_t addrlensizeof(struct sockaddr_in); while(1){ ssize_t retsendto(sockfd,buffer,strlen(buffer),0,(struct sockaddr*)srvAddr,addrlen); CHECK_RESULT(ret,发送数据失败); //5.接受服务端响应,进行展示 memset(buffer,0x00,1024); retrecvfrom(sockfd,buffer,1023,0,(struct sockaddr*)srvAddr,addrlen); CHECK_RESULT(ret,接受响应失败); printf(服务端 say:%s\n,buffer); } //6.关闭套接子 close(sockfd); return 0; }Tcp 通信程序的流程Tcp通信协议面向连接可靠传输面向字节流面向连接两端必须先建立连接才能通信可靠数据确保数据能够安全有序的到达对端面向字节流以字节为单位进行传输不限制传输大小先发先到客户端创建套接字让进程与网卡关联为套接字绑定地址信息开始监听listensockfd,int backlog告诉操作系统我可以开始接收客户端的连接请求了backlog:限制时间并发连接请求数获取新链接fd accpt(lstfd, *addr, *addrlen)将就绪的新链接从内核的就绪连接队列中取出并返回连接socket对应的描述符(通信套接字描述符).通过accept获取的新链接与客recv/sendrecv(fd, buf, len, flag);因为新建的每个通信套接字中都有完整的五元组可以通过这个socket知道与哪个客户端通信关闭套接字客户端1.创建套接字2. 为套接字绑定地址根据需要选择是否主动绑定3.连接服务器connect(fd,srvaddr, addlen)4.与服务端进行通信recv/send5.关闭套接字程序编写初步编写有瑕疵服务端1.所需的头文件#includestdio.h #includestring.h #includestdlib.h #includeunistd.h #includesys/socket.h #includenetinet/in.h #includearpa/inet.h//字节序转换2.正式代码#define CHECK_RESULT(ret,msg)\ if(ret0){perror(msg);return -1;} int main(){ //1.创建套接子 //int socket(int domian,int type,int protocol) int listen_fdsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP); CHECK_RESULT(listen_fd,创建套接子失败); //2.构造监听地址结构对象 为套接子绑定地址信息 //int bind(int sockfd,struct sockaddr*addr,socklen_t len) struct sockaddr_in addr; addr.sin_familyAF_INET; addr.sin_porthtons(9000); addr.sin_addr.s_addrinet_addr(192.168.14.128); socklen_t addrlensizeof(struct sockaddr_in); int retbind(listen_fd,(struct sockaddr*)addr,addrlen); //3.开始监听 //int listen(int sockfd,int backlog) ret listen(listen_fd,1024); CHECK_RESULT(ret,监听失败); while(1){ //4.获取新连接与客户端的通信都是通过获取新连接进行的 //int accept(listen_fd,struct sockaddr*peerAddr,socklen_t*len); //listenfd :监听套接字描述符 //*peerAddr:客户端地址信息 //*len :指定想要获取的地址长度 以及返回实际获取的地址长度 //返回值新建连接的套接子描述符用于往后的数据通信 struct sockaddr_in peerAddr; socklen_t addrlensiezeof(peerAddr); int newfdaccept(listen_fd,(struct sockaddr*)peerAddr,addrlen); if(newfd0){ perror(获取新连接出错); continue; } char peer_ip[24]{0}; inet_ntop(AF_INET,peerAddr.sin_addr,peer_ip,23);//将指定类型的地址信息 转换成字符串类型的ip地址 printf(新的客户端建立成功[%s;%d]\n, peer_ip,ntohs(peerAddr.sin_port)); //5.接收客户端消息进行展示 // ssize_t recv(int fd,char*buff,size_t len,int flag) //返回值成功则返回实际接受的数据长度 //失败出错则返回-1 若返回0 表示连接断开 char buffer[1024]{0}; retrecv (newfd,buffer,1023,0); if(ret0){ perror(接受数据出错); continue; } else if(ret0){ printf(与客户端连接断开\n); close(newfd); continue; } printf(客户端say:%s\n,buffer); //6.捕获键盘输入向客户段发送数据 memset(buffer,0x00,1024); printf(输入数据); fflush(stdout); scanf(%s,buffer); retsend(newfd,buffer,strlen(buffer),0); if(ret0){ perror(发送数据出错); colse(newfd); continue; } printf(数据发送不完整\n); } //7.关闭套接子 close(listen_fd); return 0; }客户端1.需要的头文件#includestdio.h #includestring.h #includestdlib.h #includeunistd.h #includesys/socket.h #includenetinet/in.h #includearpa/inet.h//字节序转换2.主代码#define CHECK_RESULT(ret,msg)\ if(ret0){perror(msg);return -1;} int main(){ //1.创建套接子 int sockfdsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP); CHECK_RESULT(sockfd,创建套接子失败); //2.绑定地址信息不主动 //3.向服务器发送连接请求 //int connect(int fd,struct sockaddr*addr,socklen_t len) struct sockaddr_in addr; addr.sin_familyAF_INET; addr.sin_porthtons(9000); addr.sin_addr.s_addrinet_addr(192.168.14.128); socklen_t addrlensizeof(struct sockaddr_in); int retconnect(sockfd,(struct sockaddr*)addr,addrlen); CHECK_RESULT(ret,连接服务器失败); while(1){ //4.捕获键盘数据向服务器发送数据 char buffer[1024]{0}; memset(buffer,0x00,1024); printf(输入数据); fflush(stdout); scanf(%s,buffer); retsend(sockfd,buffer,strlen(buffer),0); if(ret0){ perror(发送数据失败); close(sockfd); return -1; } //5.等待服务器响应 展示响应数据 memset(buffer,0x00,1024); retrecv(sockfd,buffer,1023,0); if(ret0){ perror(接收数据失败); colse(sockfd); return -1; } printf(服务端say:%s\n,buffer); } //6.关闭套接子 close(sockfd); return 0; }但是这个代码有瑕疵当前程序存在问题1.程序流程固定获取新连接接收数据发送数据导致同一时间只能与一个客户端通信一次循环上去就会等待获取下一个新连接当前我们的tcp客户端服务端程序在实现了之后存在问题要么服务端只能与一个客户端持续通信要么服务端只能与一个客户端通信一次原因1.因为我们的代码是串行的2.accept和recv 是阻塞接口并发tcp服务器解决方案多执行流方案1.让主控流程主线程/主进程只负责一件事获取新连接2.每获取一个新连接都为其通信创建一个新的执行流进程/线程实现每一个客户端的通信都有一个独立的执行流负责管理互不影响。方案多进程版独立性强稳定性健壮性强#includestdio.h #includestring.h #includestdlib.h #includeunistd.h #includesys/socket.h #includenetinet/in.h #includearpa/inet.h//字节序转换 #define CHECK_RESULT(ret,msg)\ if(ret0){perror(msg);return -1;} //实现一个多进程版本的服务器创建进程在子进程中与客户端进行通信 void worker(int sockfd){ pid_t pidfork(); if(pid0){ perror(创建子进程失败); return; } if(pid0){ return; } while(1){ char buffer[1024]{0}; int retrecv (sockfd,buffer,1023,0); if(ret0){ perror(接受数据出错); continue; } else if(ret0){ printf(与客户端连接断开\n); close(sockfd); break; } printf(客户端say:%s\n,buffer); //6.捕获键盘输入向客户段发送数据 memset(buffer,0x00,1024); printf(输入数据); fflush(stdout); scanf(%s,buffer); retsend(sockfd,buffer,strlen(buffer),0); if(ret0){ perror(发送数据出错); continue; } printf(数据发送不完整\n); } close(sockfd); exit(0);//当前在子进程中 这里退出的就是子进程 } int main(){ //1.创建套接子 //int socket(int domian,int type,int protocol) int listen_fdsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP); CHECK_RESULT(listen_fd,创建套接子失败); //2.构造监听地址结构对象 为套接子绑定地址信息 //int bind(int sockfd,struct sockaddr*addr,socklen_t len) struct sockaddr_in addr; addr.sin_familyAF_INET; addr.sin_porthtons(9000); addr.sin_addr.s_addrinet_addr(192.168.14.128); socklen_t addrlensizeof(struct sockaddr_in); int retbind(listen_fd,(struct sockaddr*)addr,addrlen); //3.开始监听 //int listen(int sockfd,int backlog) ret listen(listen_fd,1024); CHECK_RESULT(ret,监听失败); while(1){ //4.获取新连接与客户端的通信都是通过获取新连接进行的 //int accept(listen_fd,struct sockaddr*peerAddr,socklen_t*len); //listenfd :监听套接字描述符 //*peerAddr:客户端地址信息 //*len :指定想要获取的地址长度 以及返回实际获取的地址长度 //返回值新建连接的套接子描述符用于往后的数据通信 struct sockaddr_in peerAddr; socklen_t addrlensiezeof(peerAddr); int newfdaccept(listen_fd,(struct sockaddr*)peerAddr,addrlen); if(newfd0){ perror(获取新连接出错); continue; } char peer_ip[24]{0}; inet_ntop(AF_INET,peerAddr.sin_addr,peer_ip,23);//将指定类型的地址信息 转换成字符串类型的ip地址 printf(新的客户端建立成功[%s;%d]\n, peer_ip,ntohs(peerAddr.sin_port)); worker(newfd); return 0; } }多线程版成本低创建效率高但是稳定性稍低创建普通线程pthread_creat(tid,callback)线程分离——将当前线程分离出去线程退出后自动释放资源。这就意味着需要一个并发服务器可以与多个客户端进行通信的服务器:为每一个accept和recv/send创建一个独立的执行流-》为每一个客户端的新连接创建一个独立的执行流与其通信。#includestdio.h #includestring.h #includestdlib.h #includeunistd.h #includesys/socket.h #includenetinet/in.h #includearpa/inet.h//字节序转换 #includepthread.h #define CHECK_RESULT(ret,msg)\ if(ret0){perror(msg);return -1;} void*entry(void*arg){ int sockfd*(int*)arg;//访问地址指向的空间中的数据 while(1){ char buffer[1024]{0}; int retrecv (sockfd,buffer,1023,0); if(ret0){ perror(接受数据出错); continue; } else if(ret0){ printf(与客户端连接断开\n); close(sockfd); break; } printf(客户端say:%s\n,buffer); //6.捕获键盘输入向客户段发送数据 memset(buffer,0x00,1024); printf(输入数据); fflush(stdout); scanf(%s,buffer); retsend(sockfd,buffer,strlen(buffer),0); if(ret0){ perror(发送数据出错); continue; } printf(数据发送不完整\n); } close(sockfd); } //创建一个线程负责通过当前描述符与客户端进行通信 void worker(int sockfd){ pthread_t tid; int retpthread_creat(tid,NULL,entry,(void*)sockfd); if(ret!0){ perror(创建工作线程失败); return; } pthread_detach(tid);//分离线程让线程推出后自动释放资源不需要被等待 } int main(){ //1.创建套接子 //int socket(int domian,int type,int protocol) int listen_fdsocket(AF_INET,SOCK_STREAM,IPPROTO_TCP); CHECK_RESULT(listen_fd,创建套接子失败); //2.构造监听地址结构对象 为套接子绑定地址信息 //int bind(int sockfd,struct sockaddr*addr,socklen_t len) struct sockaddr_in addr; addr.sin_familyAF_INET; addr.sin_porthtons(9000); addr.sin_addr.s_addrinet_addr(192.168.14.128); socklen_t addrlensizeof(struct sockaddr_in); int retbind(listen_fd,(struct sockaddr*)addr,addrlen); //3.开始监听 //int listen(int sockfd,int backlog) ret listen(listen_fd,1024); CHECK_RESULT(ret,监听失败); while(1){ //4.获取新连接与客户端的通信都是通过获取新连接进行的 //int accept(listen_fd,struct sockaddr*peerAddr,socklen_t*len); //listenfd :监听套接字描述符 //*peerAddr:客户端地址信息 //*len :指定想要获取的地址长度 以及返回实际获取的地址长度 //返回值新建连接的套接子描述符用于往后的数据通信 struct sockaddr_in peerAddr; socklen_t addrlensiezeof(peerAddr); int newfdaccept(listen_fd,(struct sockaddr*)peerAddr,addrlen); if(newfd0){ perror(获取新连接出错); continue; } char peer_ip[24]{0}; inet_ntop(AF_INET,peerAddr.sin_addr,peer_ip,23);//将指定类型的地址信息 转换成字符串类型的ip地址 printf(新的客户端建立成功[%s;%d]\n, peer_ip,ntohs(peerAddr.sin_port)); worker(newfd); //线程间资源 } return 0; }
上一篇/下一篇内容由系统自动关联
返回资讯列表 →