网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“端口”可以唯一标识主机中的应用程(进程)。这样利用三元组(ip地址,协议, 端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。TCP/IP协议族包括传输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层
Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”。在许多操作系统中,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(如从套接字中读取或写入数据)。
在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:“open—write/read—close”模式。下面是网络socket通信的基本流程
socket函数
int socket(int domain,int type,int protocol); 参数: domain:指定发送通信的域 可取值:AF_UNIX:本地主机通信,与IPC类似 AF_INET:Internet地址IPV4协议 AF_INET6:Internet地址IPV6协议 type:指定socket类型 可取值:SOCK_STREAM(流套接字)、SOCK_DGRAM(数据报套接字)、SOCK_RAW(原始套接字) protocol:指定该套接字描述符上的一个特殊的协议,如TCP,UDP等,一般设为0,会自动选择type的类型对应的协议 返回值: 成功:返回创建的套接字描述符 失败:-1 补充:SOCK_STREAM(流套接字)应用TCP协议,提供顺序的,可靠的,基于字节流的双向链接 SOCK_DGRAM(数据报套接字)应用UDP协议,无链接,不可靠,不固定 SOCK_RAW(原始套接字)提供访问互联网协议和Internal Network Interfaces的权限,只有超级用户才可使用。
connect函数
TCP客户端程序调用socket创建socket_fd后,在调用connect函数来连接服务器.如果客户端这时调用connect函数发出连接请求,服务器端就会接收到这个请求并使accept函数返回,accept函数返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数 sockfd: 客户端的socket()创建的描述字 addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息 addrlen: socket地址的长度
在调用connect函数之前,还需要设置服务器的ip地址,端口信息到addr去.
//地址清零,ipv4,把端口转换网络字节序,点十进制转发为32为整形ip地址 memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); inet_aton(SERVER_IP,&serv_addr.sin_addr); rv = connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); if(rv < 0) { printf("Failed to connect server:%s\n",SERVER_IP,SERVER_PORT,strerror(errno)); return -2; } printf("Connecting to the server[%s:%d] successfully\n",SERVER_IP,SERVER_PORT);
bind函数
调用socket创建socket时,返回的socket描述字存在于协议族空间中,但是没有这个具体的地址,如果想要给它赋值一个地址,就必须调用bind函数.通常服务器在启动的时候都会绑定一个众所周知的地址(ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen函数之前会调用bind函数,而客户端就不会调用,而是在connect函数时由系统随机生成一个。当然客户端也可以在调用connect函数之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数 sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。 addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核 addrlen:对应的是addr的长度。
在调用bind函数之前,还需要设置客户端的ip地址,端口信息到addr去.
//清零地址,ipv4, 端口网络转换主机字节序,监听所以的ip转换为主机字节序 memset(&serv_addr,0 ,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(LISTEN_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(socket_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0) { printf("create socket failure:%s\n",strerror(errno)); return -2; } printf("socket[%d] bind on port [%d] for all ip address ok\n",socket_fd,LISTEN_PORT);
通用套接字sockaddr类型定义
typedef unsigned short int sa_family_t; struct sockaddr { sa_family_t sa_family; /*2字节地址族, AF_xxx */ char sa_data[14]; /*14字节的协议地址 */ }
IPv4的套接字sockaddr_in类型定义
typedef unsigned short sa_family_t; typedef uint16_t in_port_t; struct in_addr { uint32_t s_addr; }; struct sockaddr_in { sa_family_t sin_family; /* 2字节地址族,如:AF_xxx*/ in_port_t sin_port; /* 2字节端口*/ struct in_addr sin_addr; /* 4字节IPv4 地址*/ unsigned char sin_zero[8]; /*8字节未使用的填充数据,总是为零 */ };
IPv6的套接字sockaddr_in6类型定义
typedef unsigned short sa_family_t; typedef uint16_t in_port_t; struct in6_addr { union { uint8_t __u6_addr8[16]; uint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; } __in6_u; } struct sockaddr_in6 { sa_family_t sin6_family; /*2B*/ in_port_t sin6_port; /*2B*/ uint32_t sin6_flowinfo; /*4B*/ struct in6_addr sin6_addr; /*16B*/ uint32_t sin6_scope_id; /*4B*/ };
Unix域对应的sockaddr_un类型定义
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; char sun_path[UNIX_PATH_MAX]; };
listen函数
int listen(int sockfd, int backlog); 参数 sockefd: socket()系统调用创建的要监听的socket描述字 backlog: 相应socket可以在内核里排队的最大连接个数
accept函数
TCP服务器端依次调用socket函数、bind函数、listen函数之后,就会监听指定的socket地址了。服务器之后就会调用accpet函数接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect函数就会触发服务器的accept函数返回,这时整个TCP链接就建立好了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数 sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字; addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等; addrlen: 返回客户端协议地址的长度
例如代码
//监听socket,等待客户端连接,创建一个新的fd,创建新的客户端地址和客户端地址长度 client_fd = accept(socket_fd,(struct sockaddr*)&cli_addr,&cliaddr_len); if(client_fd <0) { printf("accept new socket failure :%s\n",strerror(errno)); return -2; } //将网络地址转换点十进制地址,把客户端地址的ipv4和客户端端口转发成点十进制 printf("accept new client[%s:%d] wlth fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
htons/htonl函数
意思就是把主机字节序(小端字节序)改为网络字节序(大端字节序)
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
htons:host network short(2字节/16位) ,端口号16位
htonl:host network long(4字节/32位),IP地址32位
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); 参数 htonl把端口号主机字节序转换为网络字节序 htons把ip地址主机主机序转换为网络字节序 ntohl将无符号转换整数从网络字节顺序到主机字节顺序 ntohs将无符号转换短字符从网络字节顺序到主机字节顺序 INADDR_ANY指定地址为0.0.0.0地址,表示监听所有的IP地址 hostlong:主机字节顺序表达的32位数 hostshort:主机字节顺序表达的16位数 netlong:一个以网络字节顺序表达的32位数 netshort:一个以网络字节顺序表达的16位数 serv_addr.sin_port = htons(LISTEN_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
以IP地址127.0.0.1为例
第一步 127 . 0 . 0 . 1 把IP地址每一部分转换为8位的二进制数。 第二步 01111111.00000000.00000000.00000001 = 2130706433 (主机字节序) 然后把上面的四部分二进制数从右往左按部分重新排列,那就变为: 第三步 00000001.00000000.00000000.01111111 = 16777343 (网络字节序)
以端口12345为例
第一步 00110000 00111001 = 12345 (主机字节序) 端口号其实就已经是主机字节序了,首先要把端口号写为16位的二进制数 第二步 00111001 00110000 = 14640 (网络字节序) 然后把主机字节序的前八位与后八位调换位置组成新的16位二进制数,这新的16位二进制数就是网络字节序的二进制表示了
因此,如果知道12345端口的网络字节序是14640的话serv_addr.sin_port=htons(12345)可以直接写为 serv_addr.sin_port = htons(14604) 结果是一样的,htons的作用就是把端口号主机字节序转换为网络字节序。
inet_aton和inet_ntoa函数
inet_aton函数将点十进制字符串转换成网络地址 inet_ntoa函数将网络地址转换成点十进制字符串格式
int inet_aton(const char *string, struct in_addr *addr); 将字符串表示的网络地址转换为该地址数值的整数表示,返回的数字总是按照网络字节顺序的 参数描述: 输入参数string包含ASCII表示的IP地址。 输出参数addr是将要用新的IP地址更新的结构。 返回值: 如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。 使用这个函数并没有错误码存放在errno中,所以他的值会被忽略 char *inet_ntoa(struct in_addr in) 将网络传输的二进制数值转化为成点分十进制的ip地址 返回指向点分十进制字符串的指针。 该函数将一个网络字节顺序的IP地址转换为它所对应的点分十进制串。 注意:对inet_aton的调用传递的是指向结构的指针,而对inet_ntoa的调用传递的是结构本身。
inet_pton/inet_ntop函数
inet_pton:
IP地址转换函数,支持IPv4/IPv6,可以在将IP地址从“点分十进制”转换成网络字节顺序表示Internet地址。
inet_ntop:
IP地址转换函数,支持IPv4/IPv6,可以在将网络字节顺序表示的Internet地址转换成为“点分十进制”
nt inet_pton(int af, const char *src, void *dst); 例如 inet_pton(AF_INET, "172.20.223.151", &servaddr.sin_addr); 参数 af:可以是AF_INET(对应的是ipv4)或AF_INET6(ipv6),如果,以不被支持的地址族作为family参数, 这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT. src:是一个指向点分十进制串的指针, dst:是一个指向转换后的网络字节序的二进制值的指针。 返回值 若成功则为1,若输入不是有效的表达式则为0,若出错则为-1,并将errno置为EAFNOSUPPORT. const char *inet_ntop(int af, const void *src,char *dst, socklen_t size); 例如 inet_ntop(AF_INET, &servaddr.sin_addr, IPdotdec, 16); 参数 af:可以是AF_INET(对应的是ipv4)或AF_INET6(ipv6),如果,以不被支持的地址族作为family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT. src:是一个指向点分十进制串的指针, dst:参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。 size:他是所指向缓存区dst的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC
read函数
ssize_t read(int fd , void *buf,size_t nbytes) 参数: sockfd:与远程通信连接的套接字描述符 buf:接收数据的缓冲区地址 len:缓冲区长度
read函数是负责从fd中读取内容。当读成功时read返回实际所读的字节数;如果返回的值是0表示已经读到文件的结束了,如果是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明读是由中断引起的,如果ECONNREST表示网络连接出了问题
write函数
ssize_t write(int fildes, const void *buf, size_t nbyte); args: int fildes : 写入文件的文件描述符 const void *buf: 写入数据在内存空间存储的地址 size_t nbyte : 期待写入数据的最大字节数
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。write的返回值大于0,表示写了部分或者全部的数据。返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
网络I/O操作函数有下面几组:具体参见man文档
ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t*addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
close/shutdown函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用close函数来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程
int close(int fd); int shutdown(int sockfd, int how); 参数 fd:要关闭的文件 how:值为 SHUT_RD 则该套接字不可再读入数据了 SHUT_WR 则该套接字不可再发送数据了 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了
客户端代码
#include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #define SERVER_IP "127.0.0.1" #define SERVER_PORT 12345 #define MSG_STR "Hello beautiful world" int main (int argc, char **argv) { int sockfd = -1; int rv = -1; struct sockaddr_in serv_addr; char buf[1024]; sockfd = socket(AF_INET,SOCK_STREAM,0); if(sockfd < 0) { printf("Create sockte failure:%s\n",strerror(errno)); return -1; } printf("Create sockte [%d] successful!\n",sockfd); //服务器地址清零,ipv4,把端口转发网络字节序,点十进制转发为32为整形ip地址 memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); inet_aton(SERVER_IP,&serv_addr.sin_addr); rv = connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); if(rv < 0) { printf("Failed to connect server:%s\n",SERVER_IP,SERVER_PORT,strerror(errno)); return -2; } printf("Connecting to the server[%s:%d] successfully\n",SERVER_IP,SERVER_PORT); while(1) { rv = write(sockfd,MSG_STR,strlen(MSG_STR)); if(rv < 0) { printf("Write to server[%d] successfully:%s\n",sockfd,strerror(errno)); break; } memset(buf,0,sizeof(buf)); rv = read(sockfd,buf,sizeof(buf)); if(rv <0 ) { printf("Failed to read data server:%s\n",strerror(errno)); break; } else if(0==rv) { printf("Connecting to the server:'%s'\n",rv,buf); break; } printf("read %d bytes bata from server:'%s'\n",rv,buf); close(sockfd); } return 0; }
服务器端代码
#include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define LISTEN_PORT 12345 #define BACKLOG 8 int main (int argc, char **argv) { int rv = -1; int socket_fd = -1; int client_fd = -1; struct sockaddr_in serv_addr; struct sockaddr_in cli_addr; socklen_t cliaddr_len; char buf[1024]; int on = 1; //创建socket ipv4 tcp协议 socket_fd = socket(AF_INET,SOCK_STREAM,0); if(socket_fd < 0) { printf("create socket failure:%s\n",strerror(errno)); return -1; } printf("socket create fd[%d]\n",socket_fd); //重复端口使用 setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); //清零服务器地址,ipv4, 端口网络转换主机字节序,监听所以的ip转换为主机字节序 memset(&serv_addr,0 ,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(LISTEN_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //绑定端口 if(bind(socket_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))<0) { printf("create socket failure:%s\n",strerror(errno)); return -2; } printf("socket[%d] bind on port [%d] for all ip address ok\n",socket_fd,LISTEN_PORT); //最多BACKLOG连接 listen(socket_fd,BACKLOG); while(1) { printf("\n start wainting and accept new client connect.... \n",socket_fd); //监听socket,创建一个新的fd,等待客户端连接,创建新的客户端地址和客户端地址长度,等待 client_fd = accept(socket_fd,(struct sockaddr*)&cli_addr,&cliaddr_len); if(client_fd <0) { printf("accept new socket failure :%s\n",strerror(errno)); return -2; } //将网络地址转换点十进制地址,把客户端地址的ipv4和客户端端口转发成点十进制 printf("accept new client[%s:%d] wlth fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd); memset(buf, 0 ,sizeof(buf)); rv = read(client_fd,buf,sizeof(buf)); if(rv < 0) { printf("read data from client socket[%d] failure:%s\n",client_fd ,strerror(errno)); close(client_fd); continue; } else if(rv == 0) { printf("client socket[%d] disconnected\n",client_fd); close(client_fd); continue; } printf("read %d bytes bata from client[%d] and echo it back :'%s'\n",rv ,client_fd,buf); rv = write(client_fd,buf,rv); if(rv < 0) { printf("write %d bytes data back to client[%d] failure:%s\n",rv ,client_fd,strerror(errno)); close(client_fd); } printf("close client socket[%d]\n",client_fd); close(client_fd); } close(socket_fd); return 0; }