我们知道同一台主机的进程间通信有system V共享内存,消息队列,信号量这些方式,而跨主机的进程间通信怎么搞呢?使用IP地址与端口号!
IP地址用来网络中标识唯一一台主机,是一个32位无符号整数,常常用192.163.1.1这样点分十进制的字符串形式表示。
端口号用来表示一台主机中的一个进程,它是一个16位无符号整数,所以端口号最小是0,最大是65536。那么端口号如何表示一个进程呢?如下图,端口号作为数组的下标,数组中存放的是进程PID。它相当于一个哈希表,根据下标即端口号就可以找到对应的进程。
这里有一个问题,为什么不直接用进程PID呢,非要多走一步端口号,感觉有点多此一举。
我是这样理解的,我们使用的应用程序都是有对应的服务器维护的,我们作为一个客户端需要和服务器进行数据交互,那么就必须明白两个问题,一是服务器在哪,二是与服务器的哪个进程进行通信。当我们通信之前,就必须知道服务器的IP地址与进程PID,那么我们怎么知道呢?IP地址我们可以视为客户端提前知晓且并不变更,那进程PID呢?服务器每重新打开一次进程,PID会一样吗?显然不会,那么我怎么找到服务器的对应进程呢?这里就陷入了一个死循环。
网络:请问您是要和服务器123.123.123.123通信吗?
客户端:对的。
网络:请告诉我你是要和服务器的哪个进程通信呢?
客户端:不知道啊?它的进程每次重新启动,进程号都会变更。
网络:对不起先生,没有进程号我们没法帮您通信。
客户端:我不跟服务器通信我怎么知道服务器的进程号。
当然只有ip地址也是可以接收到数据的,但是交由哪个进程处理,这些数据是什么意思用来干什么的,就成了问题。
为了避免这个问题,就有了端口号的概念。服务器的相应进程会放到一个固定的端口号上,客户端都是提前知晓这个端口号的,所以在通信时,客户端只需要端口号就可以找到对应进程。这也使得许多端口号约定成俗,比如常见的8080端口。
每台计算机的存储顺序不同,分为大端存储和小端存储。大端存储就是低字节放到高地址,小端存储就是高字节放到低地址。如下图,定义一个int num=1;
可以看到01放到高地址处的是大端存储,放到低地址处的是小端存储。
既然有这种主机存储顺序的不同,那么在进行网络通信时如果两个终端存储顺序不同,那么数据就会被错误解读。为了解决这个问题,就定义了一个共同的标准,在传输网络数据的时候都以大端存储为标准。
因为客户端发送数据,携带的目的ip与目的端口都是网络序列的,服务器端要对比数据是给哪个端口,所以本地ip和端口必须转为网络序列。
这块是关于创建套接字后,使用bind函数绑定端口号与ip的一个细节。
云服务器,或者一款服务器不要bind一个具体的ip,因为服务器可能有多个网卡多个ip地址,这些ip都有可能接收指定端口的数据,所以需要在服务器启动的时候bind任意一个ip地址,这就要求在对sockaddr里面的sin_addr里面的s_addr初始化时,使用INADDR_ANY进行初始化。
#include
#include
int socket(int domain, int type, int protocol);
socket函数用来创建一个套接字。domain选择协议家族来进行通信,ipv4网络通信使用AF_INET,ipv6使用AF_INET6。type是用来选择套接字类型的, SOCK_STREAM就是面向连接,可靠的,SOCK_DGRAM就是无连接,不可靠的。protocol用0即可,选择默认合适的协议。socket创建成功会返回一个文件描述符,创建失败返回-1。
#include
#include
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bind函数用来绑定本地主机ip与端口号。sockfd就是创建套接字成功返回的文件描述符。
我们可以看到sockaddr是个结构体,那这个结构体的成员有哪些呢?
sockaddr结构体:
__SOCKADDR_COMMON (sa_)就是#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family,其实绕来绕去就是sa_family_t sa_family,一个16位短整型变量(下面sockaddr_in结构体的第一个成员也大体一样,sa_family_t sin_family,一个16位短整型变量),用来表示地址类型如AF_INET。char sa_data[14]就是14字节的地址数据。
不过我们在进行网络通信时,使用的是sockaddr_in类型的结构体
sockaddr_in结构体:
由上图可以看出sockaddr结构体里面的sin_port是一个16位无符号整数,in_addr结构体里面有唯一一个成员---32位无符号整数。他们分别代表一个端口号和IP地址。sin_zero结构体就是填充字段,可以看到用sockaddr结构体大小减去了sockaddr_in结构体里面的三个成员的大小,最后自然sockaddr和sockaddr_in结构体的大小就一样了。这不明摆着是让sockaddr和sockaddr_in适配么。使用时直接取地址然后强转就可以了。
所以得出下面的结论:
#include
#include
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
利用创建的套接字把接收到的最大为len字节长度的数据放到buf中,flags标志位表示是否阻塞接收(设为0即可),src_addr指针和addrlen指针分别指向一个输入性参数,用来接收发送方的IP地址端口号以及结构体大小。数据成功则返回实际接收到的字符数,失败返回-1。
#include
#include
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
利用创建的套接字发送最大为len字节长度的数据,flags标志位表示是否阻塞发送(设为0即可),dest_addr指针指向一个sockaddr_in结构体(里面有目的ip和目的端口号),addrlen为该结构体大小。成功则返回实际传送出去的字符数,失败返回-1。
#include
#include
#include
in_addr_t inet_addr(const char *cp);
inet_addr() 函数将互联网主机地址 cp 字符串从 IPv4 数字和点表示法转换为按网络字节顺序的二进制数据。
#include
#include
#include
char *inet_ntoa(struct in_addr in);
inet_ntoa() 函数将按网络字节顺序给出的互联网主机地址转换为 IPv4 点分十进制表示法的字符串。 字符串以静态分配的缓冲区,后续调用将覆盖该缓冲区。不过这里的in_addr是sockaddr_in结构体里面的一个结构体成员,这个in_addr结构体里面存放的是一个32位无符号整数(IP地址)。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
已知端口号是16位无符号整数,ip地址是32位无符号整数。所以这里四个函数就是把主机字节序转换成网络字节序or网络字节序转换成主机字节序,IP地址用uint32_t,端口号用uint16_t。
代码逻辑: