Linux网络编程——网络基础编程

2019-07-12 14:58发布

一、socket概述 1.socket定义         socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。每一个socket都用一个半相关描述符{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整形的socket描述符,随后的连接建立、数据传输等操作都通过socket来实现。 2.socket类型         常见的socket有3中类型: 流式socket(SOCK_STREAM) 流式套接字提供可靠地、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。 数据报socket(SOCK_DGRAM) 数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并不保证是可靠、无差错的。它使用数据报协议UDP。 原始socket 原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。 二、地址及顺序处理 1.地址结构相关处理 (1)用来保存socket信息的两个数据结构:sockaddr和sockaddr_in: struct sockaddr { unsigned short sa_family; /*地址族*/ char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号*/ }; struct sockaddr_in { short int sa_family; /*地址族*/ unsigned short int sin_port; /*端口号*/ struct in_addr sin_addr; /*IP地址*/ unsigned char sin_zero(8); /*填充0以保持与struct sockaddr同样大小*/ }; (2)结构字段 sa_family可选值 结构定义头文件 #include sa_family AF_INET:IPv4协议 AF_INET6:IPv6协议 AF_LOCAL:UNIX协议 AF_LINK:链路地址协议 AF_KEY:密钥套接字(socket) 2.数据存储优先顺序          计算机数据存储有两种字节优先顺序:大端(高位字节优先)和小端(低位字节优先)。Internet上采用大端传输,因此需要对两个字节存储的优先顺序进行转化。Linux提供了以下四个函数: htons等函数语法要点 所需头文件 #include 函数原型 uint16_t htons(uint16_t host16bit) uint32_t htonl(uint32_t host32bit) uint16_t ntohs(uint16_t net16bit) uint32_t ntohl(uint32_t net32bit) 函数传入值 host16bit:主机字节序的16位数据 host32bit:主机字节序的32位数据 net16bit:网络字节序的16位数据 net32bit:网络字节序的32位数据 函数返回值 成功:返回要转换的字节序 出错:-1 注:h表示host,n表示network,s表示short,l表示long,通常16位端口号用s表示,32位IP地址用l表示。 3.地址格式转化 (1)函数说明         通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。其中inet_pton()函数是将点分十进制地址映射为二进制地址,inet_ntop()是将二进制地址映射为点分十进制地址。 (2)函数格式 inet_pton函数语法要点 所需头文件 #include 函数原型 int inet_pton(int family,const char *strptr,void *addrptr) 函数传入值 family AF_INET:IPv4协议 AF_INET6:IPv6协议 strptr:要转化的值 addrptr:转化后的地址 函数返回值 成功:0 出错:-1 inet_ntop函数语法要点 所需头文件 #include 函数原型 int inet_ntop(int family,void *addrptr,char *strptr,size_t len) 函数传入值 family AF_INET:IPv4协议 AF_INET6:IPv6协议 addrptr:转化后的地址 strptr:要转化的值 len:转化后值的大小 函数返回值 成功:0 出错:-1 4.名字地址转化 (1)函数说明        在Linux中,同样有一些函数可以实现主机名和地址的转化,最为常见的有gethostbyname()、gethostbyaddr()和getaddrinfo()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()是将IP地址转化为主机名,getaddrinfo()实现自动识别IPv4地址和IPv6地址。           gethostbyname()和gethostbyaddr()都涉及一个hostent的结构体,如下所示: struct hostent { char *h_name; /*正式主机名*/ char **h_aliases; /*主机别名*/ int h_addrtype; /*地址类型*/ int h_length; /*地址字节长度*/ char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/ }          getaddrinfo()函数涉及一个addrinfo的结构体,如下所示: struct addrinfo { int ai_flags; /*AI_PASSIVE,AI_CANONNAME*/ int ai_family; /*地址族*/ int ai_socktype; /*socket类型*/ int ai_protocol; /*协议类型*/ size_t ai_addrlen; /*地址字节长度*/ char *ai_canonname; /*主机名*/ struct sockaddr *ai_addr; /*socket结构体*/ struct addrinfo *ai_next; /*下一个指针链表*/ } (2)函数格式 gethostbyname函数语法要点 所需头文件 #include 函数原型 struct hostent *gethostbyname(const char *hostname) 函数传入值 hostname:主机名 函数返回值 成功:hostent类型指针 出错:-1 注:调用该函数首先对hostent结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置则默认为IPv4地址类型。 getaddrinfo()函数语法要点 所需头文件 #include 函数原型 int getaddrinfo(const char *node,const char *service,const struct addrinfo *hints,struct addrinfo **result) 函数传入值 node:网络地址或者网络主机名 service:服务名或十进制的端口号字符串 hints:服务线索 result:返回结果 函数返回值 成功:0 出错:-1 addrinfo结构体常见常数 结构体头文件 #include ai_flags: AI_PASSIVE:该套接口是用作被动地打开 AI_CANONNAME:通知getaddrinfo函数返回主机的名字 ai_family: AF_INET:IPv4协议 AF_INET6:IPv6协议 AF_UNSPEC:IPv4或IPv6均可 ai_socktype: SOCK_STREAM:字节流套接字socket(TCP) SOCK_DGRAM:数据报套接字socket(UDP) ai_protocol: IPPROTO_IP:IP协议 IPPROTO_IPV4:IPv4协议 IPPROTO_IPV6:IPv6协议 IPPROTO_UDP:UDP IPPROTO_TCP:TCP 注: (1)通常服务器端在调用getaddrinfo()之前,ai_flags设置AI_PASSIVE,用于bind()函数(用于端口和地址的绑定),主机名               nodename通常会设置为NULL。 (2)客户端调用getaddrinfo()时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(端口)则应该不为空。 (3)即使不设置ai_flags为AI_PASSIVE,取出的地址也可以被绑定,很多程序中ai_flags直接设置为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确绑定。 (3)使用实例 /* getaddrinfo.c */ #include #include #include #include #include #include #include #include int main() { struct addrinfo hints,*res = NULL; int rc; memset(&hints,0,sizeof(hints)); hints.ai_flags = AI_CANONNAME; /*通知getaddrinfo函数返回主机的名字*/ hints.ai_family = AF_UNSPEC; /*IPv4和IPv6均可*/ hints.ai_socktype = SOCK_DGRAM; /*数据报套接字socket*/ hints.ai_protocol = IPPROTO_UDP; /*UDP协议*/ rc = getaddrinfo("localhost1",NULL,&hints,&res); if(rc != 0) { perror("getaddrinfo"); exit(1); } else { printf("Host name is %s ",res->ai_canonname); /*主机名*/ } exit(0); } 三、socket基础编程 (1)函数说明         socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,函数说明如下: socket() 该函数用于建立一个socket连接,可指定socket类型等信息。在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。 bind() 该函数是用于将本地IP地址绑定到端口号,若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。 listen() 在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。 accept() 服务端程序调用listen()函数创建等待队列之后,调用accept()函数等待并接收客户端的连接请求。它通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。 connect() 该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。 send()和recv() 这两个函数分别用于发送和接收数据,可以用在TCP中,也可以用在UDP中。当用在UDP时,可以在connect()函数建立连接之后再用。 sendto()和recvfrom() 这两个函数的作用与send()和recv()函数类似,也可以用在TCP和UDP中。当用在TCP时,后面的几个与地址有关参数不起作用,函数作用等同于send()和recv();当用在UDP时,可以用在之前没有使用connect()的情况下。这两个函数可以自动寻找指定地址并进行连接。
(2)函数格式 socket()函数语法要点 所需头文件 #include 函数原型 int socket(int family,int type,int protocol) 函数传入值 family: 协议族 AF_INET:IPv4协议 AF_INET6:IPv6协议 AF_LOCAL:UNIX域协议 AF_ROUTE:路由套接字(socket) AF_KEY:密钥套接字(socket) type: 套接字类型 SOCK_STRAM:字节流套接字socket SOCK_DGRAM:数据报套接字socket SOCK_RAW:原始套接字socket protocl:0(原始套接字除外) 函数返回值 成功:非负套接字描述符 出错:-1 bind()函数语法要点 所需头文件 #include 函数原型 int bind(int sockfd,struct sockaddr *my_addr,int addrlen) 函数传入值 sockfd:套接字描述符 my_addr:本地地址 addrlen:地址长度 函数返回值 成功:0 出错:-1 listen()函数语法要点 所需头文件 #include 函数原型 int listen(int sockfd,int backlog) 函数传入值 sockfd:套接字描述符 backlog:请求队列中允许的最大请求数,大多数系统默认值为5 函数返回值 成功:0 出错:-1 accept()函数 所需头文件 #include 函数原型 int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen) 函数传入值 sockfd:套接字描述符 addr:客户端地址 addrlen:地址长度 函数返回值 成功:客户端文件描述符 出错:-1 connect()函数语法要点 所需头文件 #include 函数原型 int connect(int sockfd,struct sockaddr *serv_addr,int addrlen) 函数传入值 socket:套接字描述符 serv_addr:服务器端地址 addrlen:地址长度 函数返回值 成功:0 出错:-1 send()函数语法要点 所需头文件 #include 函数原型 int send(int sockfd,const void *msg,int len,int flags) 函数传入值 sockfd:套接字描述符 msg:指向要发送数据的指针 len:数据长度 flags:一般为0 函数返回值 成功:发送的字节数 出错:-1 recv()函数语法要点 所需头文件 #include 函数原型 int recv(int sockfd,void *buf,int len,unsigned int flags) 函数传入值 sockfd:套接字描述符 buf:存放接收数据的缓冲区 len:数据长度 flags:一般为0 函数返回值 成功:接收的字节数 出错:-1 sendto()函数语法要点 所需头文件 #include 函数原型 int sendto(int sockfd,const void *msg,int len,unsigned int flags,const struct sockaddr *to,int tolen) 函数传入值 sockfd:套接字描述符 msg:指向要发送数据的指针 len:数据长度 flags:一般为0 to:目的机的IP地址和端口号信息 tolen:地址长度 函数返回值 成功:发送的字节数 出错:-1 recvfrom()函数语法要点 所需头文件 #include 函数原型 int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen) 函数传入值 sockfd:套接字描述符 buf:存放接收数据的缓冲区 len:数据长度 flags:一般为0 from:源主机的IP地址和端口号信息 tolen:地址长度 函数返回值 成功:接收的字节数 出错:-1 (3)使用实例 服务端: /* server.c */ #include #include #include #include #include #include #include #include #define PORT 4321 //端口号 #define BUFFER_SIZE 1024 #define MAX_QUE_CONN_NM 5 int main() { struct sockaddr_in server_sockaddr,client_sockaddr; int sin_size,recvbytes; int sockfd,client_fd;//sockfd=文件描述符 char buf[BUFFER_SIZE]; /*建立socket连接*/ if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建TCP套接字 { perror("socket"); exit(1); } printf("Socket id = %d ",sockfd); /*设置sockaddr_in结构体中相关参数*/ server_sockaddr.sin_family = AF_INET; //sin_family=地址族,AF_INET=IPv4 server_sockaddr.sin_port = htons(PORT);//sin_port=端口号,htons()主机字节序转化 server_sockaddr.sin_addr.s_addr = INADDR_ANY;//s_addr=IP地址,INADDR_ANY指0.0.0.0,表示本地地址 bzero(&(server_sockaddr.sin_zero),8);//填充0以保持与struct sockaddr同样大小 int i=1;//允许重复使用本地地址与套接字进行绑定 if((setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i))) < 0)//用于任意类型、任意状态套接口的设置选项值 { perror("setsockopt"); exit(1); } /*sockfd = 标识一个套接口的描述字 level = 选项定义的层次:支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6 optname = 需设置的选项 optval = 指针,指向存放选项待设置的新值的缓冲区 optlen = optval缓冲区长度*/ /*绑定函数bind(),server_sockaddr = 本地地址,sizeof(struct sockaddr) = 地址长度*/ if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) ==-1) { perror("bind"); exit(1); } printf("Bind success! "); /*调用listen()函数,创建未处理请求的队列,MAX_QUE_CONN_NM=请求队列中允许 的最大请求数,大多数系统默认值为5*/ if(listen(sockfd,MAX_QUE_CONN_NM) == -1) { perror("listen"); exit(1); } printf("Listening... "); /*调用accept()函数,等待客户端的连接,client_sockaddr = 客户端地址,地址长度*/ if((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)) ==-1) { perror("accept"); exit(1); } /*调用recv()函数接收客户端的请求*/ memset(buf,0,sizeof(buf)); if((recvbytes = recv(client_fd,buf,BUFFER_SIZE,0)) == -1)//返回接收的字>节数 { perror("recv"); exit(1); } printf("Received a message:%s ",buf); close(sockfd); exit(0); } 客户端: /*client.c*/ #include #include #include #include #include #include #include #include #define PORT 4321 #define BUFFER_SIZE 1024 int main(int argc,char *argv[]) { int sockfd,sendbytes; char buf[BUFFER_SIZE]; struct hostent *host; struct sockaddr_in serv_addr; if(argc < 3) { fprintf(stderr,"USAGE:./client Hostname(or ip address) Test "); exit(1); } /*地址解析函数*/ if((host = gethostbyname(argv[1])) == NULL) { perror("gethostbyname"); exit(1); } memset(buf,0,sizeof(buf)); sprintf(buf,"%s",argv[2]); /*创建socket*/ if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) { perror("socket"); exit(1); } /*设置sockaddr_in 结构体中相关参数*/ serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); serv_addr.sin_addr = *((struct in_addr *)host->h_addr); bzero(&(serv_addr.sin_zero),8); /*调用connet函数主动发起对服务器端的连接*/ if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)//serv_addr = 服务器端地址 { //sizeof(struct sockaddr) = 地址长度 perror("connect"); exit(1); } /*发送消息给服务器端*/ if((sendbytes = send(sockfd,buf,strlen(buf),0)) == -1) { perror("send"); exit(1); } sleep(5); close(sockfd); exit(0); }