什么是socket?
socket可以看成是用户进程与内核网络协议栈的编程接口。是一套api函数。
socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机间的进程间通信。
工业上使用的为tcp ip四层模型,是OSI七层模型的简化,如下图所示:
tcp ip协议是每一层与每一层的通信。
IPV4套接字地质结构:
IPV4套接字地址结构通常也称为“网际套接字地址结构”,它以socketaddr_in命名,定义在头文件<netinet/in.h>中,如下所示:
struct socketaddr_in
{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中每一项的含义如下:
sin_len:整个socketaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family。
sin_family:指定该地址家族,在这里设为AF_INET,表示 ipv4协议
sin_port:端口
sin_addr:IPV4的地址
sin_zero:暂不使用,一般将其设置为0
ip用来确定主机,port用来确定哪一个应用程序。
sin_addr的结构定义如下:
struct in_addr {
uint32_t s_addr; /* address in network byte order */ //如果s_addr设置为INADDR_ANY则表示绑定本机的任意IP地址。
};
现在的内核一般没有sin_len这一项了,最后一项也没有了,而是改成了如下结构:
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 */
};
通用地址结构:
通用地址结构用来指定与套接字关联的地址。
struct sockaddr
{
uint8_t sin_len;
sa_family_t sin_family;
char sa_data[14];
};
其中,
sin_len:整个sockaddr结构的长度
sin_family:指定该地址家族
sa_data:由sin_family决定它的形式
sockaddr_in中的sin_port、sin_addr、sin_zero[8]加起来是14字节,正好和sockaddr中的sa_data对应起来。因此,sockaddr和sockaddr_in在大小上是一样的。
现在的内核中一般也没有sin_len成员了,如下所示:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
TCP IP协议族不仅支持TCP IP还支持UNIX域协议,因此,设计了一个通用的地址结构,使用时需要将具体的地址结构转换为通用地址结构,然后传给相应的API。
网络字节序的概念如下:
数据在发送之前需要转换成网络字节序,在接收端需要将网络字节序转换为本地字节序。这样才能保证数据在内存中的正确存储。
字节序转换函数:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort)
uint32_t ntohl(uint32_t netlong)
uint16_t ntohs(uint16 netshort)
在上述函数中,h代表host,n代表network,s代表short,l代表long。
从主机字节序到网络字节序的示例程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 4 #include <stdlib.h> 5 #include <stdio.h> 6 #include <string.h> 7 8 #include <signal.h> 9 #include <errno.h> 10 #include <arpa/inet.h> 11 12 int main() 13 { 14 unsigned int data = 0x12345678; 15 char *p = &data; 16 printf("%d %d %d %d\n", p[0],p[1],p[2],p[3]); 17 if(p[0] == 0x78) 18 { 19 printf("host is little end\n"); 20 } 21 else 22 { 23 printf("host is big end\n"); 24 } 25 26 uint32_t ndata = htonl(data); 27 p = &ndata; 28 printf("%d %d %d %d\n", p[0], p[1], p[2], p[3]); 29 30 if(p[0] == 0x78) 31 { 32 printf("net is little end\n"); 33 } 34 else 35 { 36 printf("net is big end\n"); 37 } 38 39 return 0; 40 }
执行结果如下:
可以看到主机字节序是小端,而网络字节序是大端。
地址转换函数:
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
这几个函数分别包含在<netinet/in.h>和<arpa/inet.h>中。
为什么要有地址转换函数呢?
因为在ipv4里面,地址是32位的,而人类习惯使用点分十进制表示ip地址,例如192.168.2.23,因此,需要在这两种表示形式间进行转换。
我们测试inet_addr,程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 4 #include <stdlib.h> 5 #include <stdio.h> 6 #include <string.h> 7 8 #include <signal.h> 9 #include <errno.h> 10 #include <arpa/inet.h> 11 #include <sys/socket.h> 12 #include <netinet/in.h> 13 14 int main() 15 { 16 in_addr_t myaddr = inet_addr("192.168.0.12"); 17 printf("%u\n", myaddr); 18 return 0; 19 }
执行结果如下:
我们测试一下inet_aton函数,程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 4 #include <stdlib.h> 5 #include <stdio.h> 6 #include <string.h> 7 8 #include <signal.h> 9 #include <errno.h> 10 #include <arpa/inet.h> 11 #include <sys/socket.h> 12 #include <netinet/in.h> 13 #include <sys/socket.h> 14 #include <netinet/ip.h> /* superset of previous */ 15 16 17 int main() 18 { 19 in_addr_t myaddr = inet_addr("192.168.0.12"); 20 printf("%u\n", myaddr); 21 22 struct in_addr inp; 23 inet_aton("192.168.0.12", &inp); 24 printf("%d\n", inp.s_addr); 25 26 return 0; 27 }
执行结果如下:
inet_aton将一个点分十进制ip地址,转换为一个struct in_addr结构,这个结构正是sockaddr_in结构中的一个成员,具体定义如下:
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
inet_aton将ip地址转换为一个struct in_addr结构,这个结构里面就是一个uint32_t的数,因此,转换出来的这个数应该和inet_addr转换出来的一样,执行结果也证明了这一点。这个inet_aton正是为了初始化sockaddr_in结构中的ip地址成员用的。
我们接下来测试一下inet_ntoa,程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <arpa/inet.h> 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #include <sys/socket.h> 11 #include <netinet/ip.h> /* superset of previous */ 12 13 int main() 14 { 15 in_addr_t myaddr = inet_addr("192.168.0.12"); 16 printf("%u\n", myaddr); 17 18 struct in_addr inp; 19 inet_aton("192.168.0.12", &inp); 20 printf("%d\n", inp.s_addr); 21 22 char *ipstr = inet_ntoa(inp); 23 printf("%s\n", ipstr); 24 25 return 0; 26 }
第22行我们将 struct in_addr的ip地址形式,转换为点分十进制,执行结果如下:
可见,转换结果是正确的。inet_ntoa不需要为字符串分配内存,这些内存是由内核为每个套接字维护的,这个函数返回一个指针,但是我们不需要释放这个指针指向的内存。
函数为什么传入的是结构而不是指针呢?这时因为这个函数需要返回一个字符串首地址,而又不想在函数内部分配内存,因此借用了参数struct in_addr的内存空间,这样可以防止内存泄漏。 因为,如果在函数中分配内存,把指针返回,是需要程序员来释放的。如果传入整个结构就避免了对返回的指针的释放,当然,程序员自己分配的内存自己释放,这就是程序员的责任了,与内核无关了。
绿色部分是错误的理解。
套接字类型:
流式套接字:
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
数据报式套接字:
提供无连接服务,不提供无错保证,数据可能丢失或者重复,并且接收顺序混乱。
原始套接字
socket编程中的客户端服务器编程模型如下:
客户端想要连接服务器,服务器必须要先把服务给架起来,也就是服务器要先建立端口,让别人可以连接。
服务器首先要建立一个socket,然后绑定地址和端口,然后开始侦听(在侦听socket或者叫被动套接字上进行侦听,这个socket只用来侦听),下一步调用accept阻塞,直到有客户端连接时,accept返回,同时返回一个新的socket套接字,这个是主动套接字,一旦建立了连接,客户端和服务器谁先发报文都可以。
socket函数:
包含在头文件<sys/socket.h>中
功能是创建一个套接字。
原型如下:
int socket(int domain, int type, int procotol)
参数:
domain:指定通信协议族
type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW
procotol:协议类型
成功返回非负整数,与文件描述符类似,称为套接口描述字,简称套接字,失败返回-1。
bind函数:
包含在头文件<sys/socket.h>中
功能是绑定一个本地地址到套接字
原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数:
sockfd:socket函数返回的套接字
addr:要绑定的地址
addrlen:地址长度
成功返回0,失败返回-1。
bind函数要求填写一个通用地址结构,一般是先定义一个ipv4地址结构,然后转换。
listen函数:
listen函数应该在调用socket和bind之后,在调用accept之前。
对于给定的监听套接字,内核要维护两个队列:
1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。
2、已完成连接的队列。
函数原型如下:
int listen(int sockfd, int backlog);
第一个参数为socket函数创建的套接字,第二个参数为最大连接数。有个宏SOMAXCONN表示最大数。
两个队列如下图所示:
一旦调用了listen函数,则socket编程被动套接字,被动套接字只能接受连接,不能主动的发起连接,调用listen后,内核会在这个socket上维护两个队列。一个是已完成连接队列,一个是未完成连接队列。
accept函数:
包含在头文件<sys/socket.h>中
功能为从已完成连接的队列中返回一个连接,如果已完成连接队列为空则阻塞。
原型如下:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
参数:
sockfd:服务器套接字
addr:将返回对等方的套接字地址
addrlen:返回对等方的套接字地址长度
成功返回非负整数,失败返回-1。
在一个套接字上进行读数据的时候,如果对方已经关闭,则tcp ip协议栈返回一个0数据包,因此read也返回0。
connect函数:
包含在头文件<sys/socket.h>中
功能是建立一个连接到addr所指定的套接字
原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
参数:
sockfd:未连接套接字
addr:要连接的地址
addrlen:第二个参数addr的长度
成功返回0,失败饭后-1。
如果调用connect之前没有绑定本机的地址和端口号,则tcp ip协议栈自动会绑定一个。
下面我们写一个简单的回射服务器程序,框图如下:
服务器端程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <arpa/inet.h> 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #include <sys/socket.h> 11 #include <netinet/ip.h> /* superset of previous */ 12 13 14 int main() 15 { 16 int sockfd = 0; 17 sockfd = socket(AF_INET, SOCK_STREAM, 0); 18 19 if(sockfd == -1) 20 { 21 perror("socket error"); 22 exit(0); 23 } 24 25 struct sockaddr_in addr; 26 addr.sin_family = AF_INET; 27 addr.sin_port = htons(8001); 28 inet_aton("192.168.31.128", &addr.sin_addr); 29 //addr.sin_addr.s_addr = inet_addr("192.168.6.249"); 30 //addr.sin_addr.s_addr = INADDR_ANY; 31 32 if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) 33 { 34 perror("bind error"); 35 exit(0); 36 } 37 38 if(listen(sockfd, SOMAXCONN) < 0) 39 { 40 perror("listen error"); 41 exit(0); 42 } 43 44 struct sockaddr_in peeraddr; 45 socklen_t peerlen; 46 47 int conn = 0; 48 49 conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen); 50 if(conn == -1) 51 { 52 perror("accept error"); 53 exit(0); 54 } 55 56 char *p = NULL; 57 int peerport = 0; 58 p = inet_ntoa(peeraddr.sin_addr); 59 peerport = ntohs(peeraddr.sin_port); 60 61 printf("peeraddr = %s\n peerport = %d\n", p, peerport); 62 63 char recvbuf[1024] = {0}; 64 int ret = 0; 65 while(1) 66 { 67 ret = read(conn, recvbuf, sizeof(recvbuf)); 68 69 if(ret == 0) 70 { 71 printf("peer closed \n"); 72 exit(0); 73 } 74 else if(ret < 0) 75 { 76 perror("read error"); 77 exit(0); 78 } 79 80 fputs(recvbuf, stdout); 81 82 write(conn, recvbuf, ret); 83 } 84 85 return 0; 86 }
要使用28行的形式初始化IP地址,使用29行形式初始化IP运行出错。
客户端程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <arpa/inet.h> 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #include <sys/socket.h> 11 #include <netinet/ip.h> /* superset of previous */ 12 13 int main() 14 { 15 int sockfd = 0; 16 sockfd = socket(AF_INET, SOCK_STREAM, 0); 17 18 struct sockaddr_in addr; 19 addr.sin_family = AF_INET; 20 addr.sin_port = htons(8001); 21 inet_aton("192.168.31.128", &addr.sin_addr); 22 //addr.sin_addr.s_addr = inet_addr("192.168.31.128"); 23 24 if( connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1 ) 25 { 26 perror("connect error"); 27 exit(0); 28 } 29 30 char recvbuf[1024] = {0}; 31 char sendbuf[1024] = {0}; 32 int ret = 0; 33 34 while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) 35 { 36 write(sockfd, sendbuf, strlen(sendbuf)); 37 38 ret = read(sockfd, recvbuf, sizeof(recvbuf)); 39 40 fputs(recvbuf, stdout); 41 memset(recvbuf, 0, sizeof(recvbuf)); 42 memset(sendbuf, 0, sizeof(sendbuf)); 43 44 } 45 46 return 0; 47 }
注意20行要使用htons,使用htonl出错。服务器绑定地址时一定要绑定本机的地址,不能是随意地址,否则bind出错。
执行结果如下:
先启动服务器
再启动客户端
客户端发送的信息都被回射回来,服务器端也成功的接收到了数据。
客户端和服务器保持着连接,我们启动第三个终端,查看套接字状态,如下所示:
处于LISTEN状态的就是被动套接字。ESTABLISHED是已经建立的连接。因为服务器和客户端放在了一台机器上,所以出现了两条ESTABLISHED连接。
中间的IP地址和端口表示本端的信息,下一列的IP地址和端口表示对端的信息。因此,上面图中的第一行表示服务器侦听套接字的信息,本端的地址是192.168.31.128:8001, 对端地址不知道,所示是0.0.0.0:*。第二条表示客户端信息,本端地址为192.168.31.128:57991,对端地址是192.168.31.128:8001。第三条表示服务器端另一个套接字的信息。
现在我们把服务器端先关掉,在查看套接字状态:
可以看到连接的状态发生了变化。
我们在两个终端起两次服务器,第二次起服务器时出错,如下所示:
因为8001端口和地址已经绑定了,不能重复绑定。
下一个实验:1、启动服务器;2、启动客户端;3、关闭服务器;4、迅速启动服务器,实验结果如下:
依然出现了端口被占用的错误提示,这是因为刚结束服务器之后,其中的侦听套接字还处于FIN_WAIT2,TCP IP规定套接字处于FIN_WAIT2或者TIME_WAIT时,这个套接字暂时不能被再次绑定。
如果一定要绑定怎么办呢?
可以使用地址复用技术:
服务器端尽可能使用SO_REUSEADDR
在绑定之前尽可能调用setsockopt来设置SO_REUSEADDR套接字选项,使用这个选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器。
修改服务器程序如下:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <arpa/inet.h> 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #include <sys/socket.h> 11 #include <netinet/ip.h> /* superset of previous */ 12 13 14 int main() 15 { 16 int sockfd = 0; 17 sockfd = socket(AF_INET, SOCK_STREAM, 0); 18 19 if(sockfd == -1) 20 { 21 perror("socket error"); 22 exit(0); 23 } 24 25 struct sockaddr_in addr; 26 addr.sin_family = AF_INET; 27 addr.sin_port = htons(8001); 28 inet_aton("192.168.31.128", &addr.sin_addr); 29 //addr.sin_addr.s_addr = inet_addr("192.168.6.249"); 30 //addr.sin_addr.s_addr = INADDR_ANY; 31 32 int optval = 1; 33 if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) 34 { 35 perror("setsockopt error"); 36 exit(0); 37 } 38 39 if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) 40 { 41 perror("bind error"); 42 exit(0); 43 } 44 45 if(listen(sockfd, SOMAXCONN) < 0) 46 { 47 perror("listen error"); 48 exit(0); 49 } 50 51 struct sockaddr_in peeraddr; 52 socklen_t peerlen; 53 54 int conn = 0; 55 56 conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen); 57 if(conn == -1) 58 { 59 perror("accept error"); 60 exit(0); 61 } 62 63 char *p = NULL; 64 int peerport = 0; 65 p = inet_ntoa(peeraddr.sin_addr); 66 peerport = ntohs(peeraddr.sin_port); 67 68 printf("peeraddr = %s\n peerport = %d\n", p, peerport); 69 70 char recvbuf[1024] = {0}; 71 int ret = 0; 72 while(1) 73 { 74 ret = read(conn, recvbuf, sizeof(recvbuf)); 75 76 if(ret == 0) 77 { 78 printf("peer closed \n"); 79 exit(0); 80 } 81 else if(ret < 0) 82 { 83 perror("read error"); 84 exit(0); 85 } 86 87 fputs(recvbuf, stdout); 88 89 write(conn, recvbuf, ret); 90 } 91 92 return 0; 93 }
添加了32-37行,重复上面的实验,发现在服务器关掉后可以立即重启了,但是客户端不能发数据过来,客户端重启后才可以发数据。
下一个小实验:1、启动服务器;2、启动客户端;3、在启动一个相同的客户端
第一个客户端可以正常的发数据给服务器,但是第二个客户端发数据没有反应,这是因为服务器只使用了一个进程,accept取出一个套接字后,这个进程就和客户端进行通信了,侦听套接字还会一直在侦听(由内核负责),但是后面来的连接成功的套接字只是存在了内核中该侦听套接字的已连接队列中,并没有取出来,因为进程不会在执行到accept处了,因为进程正在和第一个连接的客户端通信。
这个进程在和客户端通信的时候,侦听也是存在的,只是由内核协议栈负责3次握手,并把连接放到队列,只是这个连接没有取出来。
这些我们可以通过netstat -na | grep 8001查看套接字状态得出,如下所示:
可以看到第二个连接也已经建立了。
要想解决上述问题,我们需要引入并发模型,服务器需要开启多个进程或者线程。修改服务器程序,如下所示:
1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <arpa/inet.h> 8 #include <sys/socket.h> 9 #include <netinet/in.h> 10 #include <sys/socket.h> 11 #include <netinet/ip.h> /* superset of previous */ 12 13 14 int main() 15 { 16 int sockfd = 0; 17 sockfd = socket(AF_INET, SOCK_STREAM, 0); 18 19 if(sockfd == -1) 20 { 21 perror("socket error"); 22 exit(0); 23 } 24 25 struct sockaddr_in addr; 26 addr.sin_family = AF_INET; 27 addr.sin_port = htons(8001); 28 inet_aton("192.168.31.128", &addr.sin_addr); 29 //addr.sin_addr.s_addr = inet_addr("192.168.6.249"); 30 //addr.sin_addr.s_addr = INADDR_ANY; 31 32 int optval = 1; 33 if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) 34 { 35 perror("setsockopt error"); 36 exit(0); 37 } 38 39 if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) 40 { 41 perror("bind error"); 42 exit(0); 43 } 44 45 if(listen(sockfd, SOMAXCONN) < 0) 46 { 47 perror("listen error"); 48 exit(0); 49 } 50 51 struct sockaddr_in peeraddr; 52 socklen_t peerlen; 53 54 int conn = 0; 55 56 char *p = NULL; 57 int peerport = 0; 58 p = inet_ntoa(peeraddr.sin_addr); 59 peerport = ntohs(peeraddr.sin_port); 60 61 char recvbuf[1024] = {0}; 62 int ret = 0; 63 pid_t pid = 0; 64 while(1) 65 { 66 conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen); 67 if(conn == -1) 68 { 69 perror("accept error"); 70 exit(0); 71 } 72 73 pid = fork(); 74 75 if(pid == 0) 76 { 77 printf("peeraddr = %s\n peerport = %d\n", p, peerport); 78 close(sockfd); 79 while(1) 80 { 81 ret = read(conn, recvbuf, sizeof(recvbuf)); 82 83 if(ret == 0) 84 { 85 printf("peer closed \n"); 86 exit(0); 87 } 88 else if(ret < 0) 89 { 90 perror("read error"); 91 exit(0); 92 } 93 94 fputs(recvbuf, stdout); 95 96 write(conn, recvbuf, ret); 97 } 98 } 99 else if(pid > 0) 100 { 101 close(conn); 102 } 103 else 104 { 105 perror("fork error"); 106 close(conn); 107 close(sockfd); 108 exit(0); 109 } 110 111 112 } 113 114 close(conn); 115 close(sockfd); 116 117 return 0; 118 }
执行程序,结果如下:
可以看到两个客户端可以同时和服务器通信了。