第十六章:网络IPC 套接字
一、IP地址和端口
套接字接口可以用于计算机间通信。目前计算机间使用套接字通讯需要保证处于同一网段。
为了查看是否处于同一网段,我们可以使用IP地址判断。
IP地址是计算机在网络中的唯一标识。IP地址本质是个整数,它与网卡的物理地址(MAC地址)绑定。MAC地址在网卡出厂时都确保唯一,不需要我们关心。
IP地址有IPv4和IPv6之分,IPv4是32位整数,IPv6是128位整数。现在使用的一般是IPv4。
为了便于记忆,IPv4地址的每个字节转换为一个整数(8位整数,0到255),各个整数之间使用“.”隔开,这种方式叫点分十进制。如192.168.7.5。
人类使用的是点分十进制,计算机底层存储的是整数格式(十六进制)。这两种表示方法在编程时需要互相转换。
当我们上网时输入网址如baidu.com,这个baidu.com域名是IP地址的助记符。域名需要转换成IP地址才能找到计算机,这个工作由域名解析服务器完成。
在Linux中,我们可以使用ifconfig查看当前计算机的IP地址
上图中IPv4为127.0.0.1的lo为本地回环,简单点说就是用于本机通讯。
可以看到我的系统的IPv4地址为7.7.7.5,若有和我处于一个网络电脑的IPv4地址为7.7.7.3。我们需要使用位与判断这两个IP地址是否处于同一网段。
首先把7.7.7.5位与Mask(255.0.0.0),得到7.0.0.0;
之后把7.7.7.3位与Mask(255.0.0.0),得到7.0.0.0。
可以发现结果一致,表示两IP处于同一网段。
但是IP地址只能定位计算机,不能定位计算机中的进程。为解决此问题,系统提供了端口。
端口用于计算机对外管理进程。因此网络编程需要提供IP地址和端口号。
端口的本质是一个unsigned short(0到65535),代表了计算机中的每一个进程。这些进程中,有些端口已经被占用,编程中需要使用未被占用的端口。
0 到 1023:系统预先占用部分,最好别用
1024 到 48000:可以使用,但有个别端口被其它程序占用
48000 到 65535:系统可能会随时使用某一个,不稳定
二、字节顺序
有了IP和端口后,传输的源头和目的地就有了,此时还需要数据和数据格式。
在网络中,数据格式并不是确定的,因此我们在发送或接受数据时,需要把本机格式转换网络格式或把网络格式还原本机格式。
IP本机格式转换网络格式使用函数为inet_addr(),如:inet_addr("7.7.7.5");
IP网络格式转换本机格式使用函数为inet_ntoa(),如:inet_ntoa(sockaddr.sin_addr);
端口本机格式转换网络格式使用函数为htons(),如:htons(8888);
三、套接字的编程步骤
网络编程需要考率两个方面:服务器端和客户端。
服务器端的编程步骤:
1. 调用socket()函数,创建一个socket描述符,它和fd使用方法一致
2. 配置struct sockaddr_in或struct sockaddr_un,准备进行数据交互
3. 调用bind()函数,绑定通信地址和socket描述符
4. 调用read()、write(),读写socket描述符
5. 调用close()函数,关闭socket描述符
客户端的编程步骤:
除了服务器端步骤3的bind()换成connect()以外,其它步骤和服务器端一样,并且connect()和bind()用法完全一样。
套接字类函数和结构体定义如下:
1. 创建socket描述符
#include <sys/types.h> #include <sys/socket.h> /* 创建socket描述符 */ int socket(int domain, int type, int protocol);
函数参数以及返回值:
domain:选择协议,有以下宏:
AF_UNIX/AF_LOCAL/AF_FILE:本地通信
AF_INET:网络通信IPv4
AF_INET6:网络通信IPv6
type:通信类型,有以下宏:
SOCK_STREAM:数据流,用于TCP(有数据回传机制,可以判断是否发送成功)
SOCK_DGRAM:数据报,用于UDP(没有数据回传机制)
返回值:成功返回socket描述符;出错返回-1。
2. 配置struct sockaddr_in或struct sockaddr_un
其实通信使用的结构体是sockaddr。它是sockaddr_in和sockaddr_un的一般化,sockaddr_in和sockaddr_un做参数时必须转化为sockaddr。
在代码中,sockaddr_in负责网络通信;sockaddr_un负责本地通信。
其中sockaddr_un结构体定义如下:
#include <sys/un.h> struct sockaddr_un { int sun_family; // 用于指定协议,和socket()的第一个参数保持一致 char sun_path[]; // 存socket文件名(做交互媒介) // 注意,数组不能直接用=赋值 }; /* 示例 */ struct sockaddr_un addr; addr.sun_family = AF_UNIX; strcpy(addr.sun_path, "a.sock"); // 系统会自动创建a.sock文件
sockaddr_in结构体定义如下:
#include <netinet/in.h> struct sockaddr_in { int sin_family; // 用于指定协议,和socket()的第一个参数保持一致 short sin_port; // 端口号 struct in_addr sin_addr; // 存储IP地址的结构 }; /* 示例 */ struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8888); addr.sin_addr.s_addr = inet_addr("7.7.7.5");
3. 绑定socket描述符和struct sockaddr
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* 示例 */ bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
四、TCP通信
由于TCP会进行回传工作等,因此TCP在前三步的基础上,需要监听客户端回应并等待客户端连接。
服务器端的编程步骤:
1. 调用socket()函数,创建一个socket描述符,它和fd使用方法一致
2. 配置struct sockaddr_in或struct sockaddr_un,准备进行数据交互
3. 调用bind()函数,绑定通信地址和socket描述符
4. 调用listen()函数,监听客户端
5. 调用accept()函数,等待客户端连接。accept()会返回客户端的socket描述符,用于读写交互
6. 调用read()、write(),读写socket描述符
7. 调用close()函数,关闭socket描述符
客户端的编程步骤:
与之前一致
listen()函数定义如下:
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); /* 示例 */ listen(sockfd, 100); // 监听100个客户端
accept()函数定义如下:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); /* 示例 */ /* 客户端fd 返回的客户端addr */ clientfd = accept(sockfd, (struct sockaddr*)&client, &clientlen);
服务器示例代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 #include <arpa/inet.h> 7 #include <unistd.h> 8 9 int main() 10 { 11 int id = socket(AF_INET, SOCK_STREAM, 0); 12 if (id == -1) 13 perror("socket"), exit(-1); 14 15 struct sockaddr_in addr; 16 addr.sin_family = AF_INET; 17 addr.sin_port = htons(2222); 18 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 /* 防止SIGINT后一段时间端口占用 */ 21 int reuse = 1; 22 setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); 23 24 int res = bind(id, (struct sockaddr*)&addr, sizeof(addr)); 25 if (res == -1) 26 perror("bind"), exit(-1); 27 printf("绑定完成!\n"); 28 29 listen(id, 10); 30 31 int client; 32 struct sockaddr_in from; 33 socklen_t len = sizeof(from); 34 35 client = accept(id, (struct sockaddr*)&from, &len); 36 if (client == -1) 37 perror("accept"), exit(-1); 38 printf("%s连接了\n", inet_ntoa(from.sin_addr)); 39 40 char buf[100]; 41 read(client, buf, sizeof(buf)); 42 printf("接收数据为:%s\n", buf); 43 44 close(client); 45 46 return 0; 47 }
客户端示例代码如下:
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <arpa/inet.h> 7 #include <unistd.h> 8 9 int main() 10 { 11 int fd = socket(AF_INET, SOCK_STREAM, 0); 12 if (fd == -1) 13 perror("socket"), exit(-1); 14 15 struct sockaddr_in sock; 16 sock.sin_family = AF_INET; 17 sock.sin_port = htons(2222); 18 sock.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 int res = connect(fd, (struct sockaddr *)&sock, sizeof(sock)); 21 if (res == -1) 22 perror("connect"),exit(-1); 23 printf("连接成功\n"); 24 25 char buf[100]; 26 27 strcpy(buf, "Hello World"); 28 29 res = write(fd, buf, strlen(buf)); 30 printf("发送成功\n"); 31 32 close(fd); 33 34 return 0; 35 }
测试时首先启动服务器程序,之后启动客户端。
五、UDP通信
服务器端的编程步骤:
1. 调用socket()函数,创建一个socket描述符,它和fd使用方法一致
2. 配置struct sockaddr_in或struct sockaddr_un,准备进行数据交互
3. 调用bind()函数,绑定通信地址和socket描述符
4. 调用read()或recvfrom()读socket描述符,调用sendto()写socket描述符
5. 调用close()函数,关闭socket描述符
客户端的编程步骤:
不需要之前的connect()函数;调用recvfrom()、sendto(),读写socket描述符
read()和recvfrom()的区别在于:
read()只能读数据,不能读发送方的通信地址,而recvfrom()两者皆可
recvfrom()和sendto()函数定义如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
其中,
参数flags用于指定是否阻塞,如不需要阻塞等待可设置为MSG_DONTWAIT,阻塞等待可设置为0。
参数src_addr表示发送数据的地址;参数dest_addr表示接收数据的地址。
需要注意的是recvfrom()函数的最后一个参数采用的是指针传递,而sendto采用的值传递。
服务器示例代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 #include <arpa/inet.h> 7 #include <unistd.h> 8 9 int main() 10 { 11 int id = socket(AF_INET, SOCK_DGRAM, 0); 12 if (id == -1) 13 perror("socket"), exit(-1); 14 15 struct sockaddr_in addr; 16 addr.sin_family = AF_INET; 17 addr.sin_port = htons(2222); 18 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 /* 防止SIGINT后一段时间端口占用 */ 21 int reuse = 1; 22 setsockopt(id, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); 23 24 int res = bind(id, (struct sockaddr*)&addr, sizeof(addr)); 25 if (res == -1) 26 perror("bind"), exit(-1); 27 printf("绑定完成!\n"); 28 29 int client; 30 struct sockaddr_in from; 31 socklen_t len = sizeof(from); 32 33 char buf[100]; 34 recvfrom(id, buf, sizeof(buf), 0, (struct sockaddr*)&from, &len); 35 printf("%s连接了,接收数据为:%s\n", inet_ntoa(from.sin_addr), buf); 36 37 close(client); 38 39 return 0; 40 }
客户端示例代码如下:
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <arpa/inet.h> 7 #include <unistd.h> 8 9 int main() 10 { 11 int fd = socket(AF_INET, SOCK_DGRAM, 0); 12 if (fd == -1) 13 perror("socket"), exit(-1); 14 15 struct sockaddr_in sock; 16 sock.sin_family = AF_INET; 17 sock.sin_port = htons(2222); 18 sock.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 char buf[100]; 21 22 strcpy(buf, "Hello World"); 23 24 int res = sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&sock, sizeof(sock)); 25 printf("发送成功\n"); 26 27 close(fd); 28 29 return 0; 30 }
本章给出的示例代码过于简单,以后我会融合信号、多线程和线程同步知识,编写一个服务器支持多客户端示例。