C/C++服务器开发
网络与通信Socket
Socket通信三要素:通信的目的地址、使用的端口号(http 80 / smtp 25)、使用的传输协议(TCP、UDP)。
nslookup xx 可以查询xx网址的IP地址。
Socket通信模型
telnet ipxx 进行主机间通信。
一个简单的服务器和客户端通信程序,服务器端代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <ctype.h> #include <arpa/inet.h> #define SERVER_PORT 666 int main(void) { int sock; struct sockaddr_in server_addr; sock = socket(AF_INET, SOCK_STREAM, 0); // printf("wait \n"); bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(sock, 128); printf("wait client connect\n"); int done = 1; while (done) { struct sockaddr_in client; int client_sock, len, i; char client_ip[64]; char buf[256]; socklen_t client_addr_len; client_addr_len = sizeof(client); client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len); printf("client ip: %s \t port is : %d \n", inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client.sin_port)); len = read(client_sock, buf, sizeof(buf) - 1); buf[len] = '\0'; printf("receive[%d]:%s\n", len, buf); len = write(client_sock, buf, len); printf("finish. len:%d\n", len); close(client_sock); } close(sock); return 0; }
客户端代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_PORT 666 #define SERVER_IP "127.0.0.1" int main(int argc, char *argv[]){ int sockfd; char *message; struct sockaddr_in servaddr; int n; char buf[64]; if(argc != 2){ fputs("Usage: ./echo_client message \n", stderr); exit(1); } message = argv[1]; printf("message: %s\n", message); sockfd = socket(AF_INET, SOCK_STREAM, 0); //重置结构体的内存空间 memset(&servaddr, '\0', sizeof(struct sockaddr_in)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr); servaddr.sin_port = htons(SERVER_PORT); connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); write(sockfd, message, strlen(message)); n = read(sockfd, buf, sizeof(buf)-1); if(n>0){ buf[n]='\0'; printf("receive: %s\n", buf); }else { perror("error!!!"); } printf("finished.\n"); close(sockfd); return 0; }
Socket概念
socket(套接字)的中文意思为插座,socket一般用整型表示,Linux中,表示进程x间网络通信的特殊文件类型。本质上为内核借助缓冲区形成的为文件。可以使用文件描述符引用套接字。Linux系统将其封装成文件的目的是为了统一接口,使读写套接字和读写文件操作一致。区别在于文件主要因用于本地持久化数据的读写,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,IP地址-TCP或UDP端口号 唯一标识网络通讯中的一个进程。IP地址+端口号 就对应一个socket。与建立连接的两个进程各有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
网络通讯中,套接字一定是成对出现的。一段的发送缓冲区对应另一端的接收缓冲区。使用同一个文件描述符发送缓冲区和接收缓冲区。
服务器和客户端之间的通讯是全双工的,可以互相读写,采用同步和异步的方式进行交互。
四次挥手结束客户端和服务器端的通讯。
网络字节序
大端字节序-低地址高字节,高地址低字节。
小端字节序-低地址低字节,高地址高字节。
内存中的多字节数据相对于内存地址、磁盘文件中的多字节数据相对于文件中的偏移地址,网络数据流都有大端和小端之分。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节一次保存在接受缓冲区中,也是按照内存地址从低到高的顺序保存。因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,既低地址高字节。
32位IP地址也要考虑网络字节序和主机字节序的问题。C/C++中采用一下库函数进行网络字节序和主机字节序的转换。
//头文件,库函数 #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);
h表示host,n表示network,l表示32位长整型,s表示16位短整型。
如果主机是小端字节序,这些函数将参数做相应的大小端转化然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
SocketAddr详解
很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
SocketAddress结构图
struct sockaddr { sa_family_t sa_family;/* address family, AF_xxx AF_INET(IPV4) AF_INET(IPV6)*/ char sa_data[14]; /* 14 bytes of protocol address */ }; struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
IPv4的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些像bind 、accept函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
struct sockaddr_in servaddr; bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* initialize servaddr */
IP地址转化
#include <arpa/inet.h> //将字符串的IP转化为网络的整型IP int inet_pton(int af, const char *src, void *dst); //将网络字节序的IP转化为字符串的IP const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af 取值可选为 AF_INET 和 AF_INET6 ,即和 ipv4 和ipv6对应支持IPv4和IPv6假设主机地址位2.3.4.5,其中2表示低位,5表示高位,则大端字节序的结果为5040302(使用inet_pton(AF_INET,"2.3.4.5",&s_add)进行转化),小端字节序为2030405(使用ntohl(s_addr)进行转化)。
ipconfig /all查看主机的网络地址。
Socket编程
socket函数
//头文件 #include<sys/types.h> #include<sys/socket.h> int socket(int doamin,int type,int protocol); domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPV4的地址。 AF_INET6 和AF_INET类似,不过是用来IPV6的地址。 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台主机及同时使用的的协议。 type: SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完成的基于字节流的连接。这是一个使用最多的socket类型,用于TCP进行传输的。 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行连接。 SOCK_SEQPACKET 该协议是双线路的,可靠的链接,发送固定长度的数据包进行传输。必须把这个包完整接受才能进行读取。 SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) SOCK_RDM 这个类型是很少使用的,在大部分操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。 protocol: 传0表示使用默认协议 return: 成功:返回只想新创建的socket的文件描述符。失败:返回-1,并设置errno。
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以向读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。
bind函数
服务器将程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号就可以向服务器发起连接,因此服务器需要调用bind进行绑定。
//头文件 #include<sys/type.h> #include<sys/socket.h> int bind(int sockfd,const struct sockaddr *addr,socklen_t addren); sockfd: socket文件描述符 addr: 购找出IP地址加端口号 addrlen: sizeof(addr)长度 return: 成功返回0。失败返回-1,设置errno。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
struct sockaddr_in servaddr; //结构体清空很重要 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);
Listen函数
典型的服务器程序可以同时服务与多个客户端,当有客户端发起连接时,服务器调用的accept()函数返回并接受这个连接,如果有大量的可u段发起连接而服务器来不及处理,桑威accept的客户端就处于这个连接等待状态,listen()僧名sockfd处于监听状态,如果接受到更多的连接请求就忽略,listen()成功返回0,失败返回-1.
//头文件 #include<sys/types.h> #include<sys/socket.h> int listen(int sockfd,int backlog); sockfd: socket文件描述符 backlog: 在Linux系统中,它是指排队等待建立3次握手队列长度
查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
accept函数
//头文件 #include<sys/types.h> #include<sys/socket.h> int accept(int sockfd,struct aockaddr *addr,socklen_t *addrlen); sockdf: socket文件描述符 addr: 传出的参数,返回链接客户端地址信息,含IP地址和端口号 addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接受到地址结构体的大小 return: 返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,并设置errno
三次握手以后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
服务器端代码结构案例:
while (1) { cliaddr_len = sizeof(cliaddr); //如果没有客户端连接就会一直堵塞在这个代码上,不会往下进行执行 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ....... close(connfd); }
整个结构是一个while死循环,每次循环处理一个客户端连接,由于cliaddr_len是一个传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfs是先前监听的文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就是通过或者connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept参数。accept()成功返回一个文件描述符,出错返回-1。
connect函数
//头文件 #include<sys/types.h> #include<sys/socket.h> int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen); sockdf: socket文件描述符 addr: 传入参数,指定服务器端地址信息,含IP地址和端口号 addrlen: 传入参数,指定服务器段地址信息,含IP地址和端口信息 return: 成功返回0,失败返回-1,并设置errno
客户端需要调用connect()连接服务器,connect和bind的参数一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
出错处理函数
系统调用不能保证每次执行都成功,应该尽快获得程序故障信息。
//头文件 #include<errno.h> #include<string.h> char *strerror(int errnum) errnum: 传入参数,错误编号的值,一般去errno的值 return: 错误原因 #include<stdio.h> #include<errno.h> void perror(const char *s); s: 传入参数,自定义描述 return: 无 向标准出错stdeer输出出错原因(控制台打印)。