进程通信的概念最初来源于单机系统,由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如:管道(pipe)、消息(message)、共享存储区(shared memory)和信号量(semaphore)等。
但是这都仅限于用在本机进程之间通信。网络间进程通信要解决的是不同主机进程间的相互通信问题为此,引入了套接字。
套接字(socket),在Linux环境下,用于表示进程间通信的特殊文件类型(伪文件)。我们知道,在TCP/IP协议中:
IP地址:在网络环境中唯一标识一台主机
端口号:在主机中唯一标识一个进程
IP地址+端口号:在网络环境中唯一标识一个进程
这个在网络中被唯一标识的进程,被称为socket。在网络通信中,套接字一定是成对出现的。这两个socket组成的socket pair就唯一标识一个连接。
在TCP/IP模型中,套接字位于应用层和传输层之间:
套接字一般分为以下三种类型:
流式套接字(SOCK_STREAM)
提供可靠的、面向连接的通信流,通过它发送的数据保证原有顺序不变。它使用的是TCP协议。
数据报套接字(SOCK_DGRAM)
定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠、无差错。它使用的是UDP协议。
原始套接字(SOCK_RAW)
允许对底层的协议直接访问,主要用于新的网络协议的开发。它功能强大,但使用复杂。
我们知道,计算机在内存中存放数据有小端字节序(Little-Endian)和大端字节序(Big-Endian)两种方法。举个简单的例子,对于整型数据0x12345678,有以下两种存放形式:
TCP/IP协议规定了,网络数据应采用大端字节序。
因此如果主机是大端字节序,网络传输时不要做转换;如果主机是小端字节序,网络传输时就需要做转换。
为了使网络程序具有可移植性,编写socket程序可以使用以下函数来进行网络字节序和主机字节序的转换:
#include/* h表示host,n表示network,l表示32位长整数,s表示16位短整数。 这些函数将参数转换为大端字节序并返回。 */ /* 主机字节序转网络字节序(32位,用于ip地址)*/ uint32_t htonl(uint32_t hostlong); /* 主机字节序转网络字节序(16位,用于端口号)*/ uint16_t htons(uint16_t hostshort); /* 网络字节序转主机字节序(32位,用于ip地址)*/ uint32_t ntohl(uint32_t netlong); /* 网络字节序转主机字节序(16位,用于端口i号)*/ uint16_t ntohs(uint16_t netshort);
以IPv4为例,我们平时使用的ip地址像192.168.1.1这种形式,属于点分十进制字符串,但是计算机内部进行传输时则需要将其转换为32位的二进制,还要考虑主机字节序和网络字节序的转换问题。
Linux系统为了简化我们进行socket编程时对ip地址的操作,为我们提供了以下两个ip地址转换函数:
#include/* p代表ip,n代表net。 兼容了IPV4和IPV6 ip地址的字符串类型(点分十进制)和整型(二进制)的转换,内部已经做了网络字节序的转换。 */ /* af:指定了是IPV4(AF_INET)还是IPV6(AF_INET6) src:源ip地址(字符串) dst:目标ip地址 */ int inet_pton(int af, const char *src, void *dst); /* af:指定了是IPV4(AF_INET)还是IPV6(AF_INET6) src:源ip地址 dst:目标ip地址(字符串) size:dst的大小 */ const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
注:早期的ip转换函数已被废弃,被废弃的函数包括:
/* 以下ip地址转换函数只能处理IPV4的地址,已被废弃 */ #include#include #include 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);
struct sockaddr是一个非常古老的结构体,诞生早于IPv4协议。后来随着网络的发展,不得不对该结构体进行细分,划分出了以下三种:
struct sockaddr_in
struct sockaddr_in6
struct sockaddr_un
为了不改变那些使用struct sockaddr作为参数的函数(例如bind、accpet、connect),struct sockaddr退化成了泛型(和 void * 作用相似)。
因此,我们编程时需要根据具体的需求,来确定要定义哪种被细分的struct sockaddr结构体,然后在函数中将其强制转换为struct sockaddr类型。
注:struct sockaddr现在已经被废弃了,不能使用!
比如在bind()函数中:
//假设地址族协议为AF_INET struct sockaddr_in serv_addr; bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
因为目前IP地址仍然主要使用IPv4协议,因此struct sockaddr_in结构体是我们当前最为常用的struct sockaddr结构体,其定义如下:
struct sockaddr_in { /* 地址族协议 可选为 AF_INET 或 AF_INET6 */ sa_family_t sin_family; in_port_t sin_port; /* 端口号 */ struct in_addr sin_addr; /* 网络地址结构体 */ }; struct in_addr { uint32_t s_addr; /* 网络地址 */ };
其实struct sockaddr_in内部不止这些属性,但我们只需关心这些。
我们发现sin_addr是struct in_addr类型,最恶心的是该结构体内部只有一个成员。
#include#include int socket(int domain, int type, int protocol);
成功返回指向新创建的socket的文件描述符。失败返回-1,错误信息存放于errno。
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读文件一样用read/write在网络上收发数据。
1)domain:地址族协议,常用的有以下几个参数:
参数 | 含义 |
---|---|
AF_INET | 最常用,使用TCP或UDP来进行传输,使用IPv4的地址 |
AF_INET6 | 与上面类似,但使用IPv6的地址 |
AF_UNIX | 本地协议,使用在本地Unix和Linux系统上 |
2)type:协议类型,常用的有以下几个参数:
参数 | 含义 |
---|---|
SOCK_STREAM | 该协议是按照顺序的、可靠的、数据完整的基于字节流的连接,使用TCP进行传输 |
SOCK_DGRAM | 该协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP进行传输 |
SOCK_SEQPACKET | 该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须完整接收包才能进行读取 |
SOCK_RAW | socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议 |
SOCK_RDM | 很少使用,提供给数据链路层使用,不保证数据包的顺序 |
3)protocol:传0即可,表示使用每种协议的默认协议(即SOCK_STREAM使用TCP,SOCK_DGRAM使用UDP)。
#include#include /* sockfd: socket文件描述符 addr: 构造出IP地址加端口号 addrlen: add的长度 */ int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号就可以向服务器发起连接,因此服务器需要调用bind()绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
前面说过,struct sockaddr * 相当于一个泛型,因此其具体实现的长度各不相同,所以需要第三个参数指定结构体的长度。
成功返回0。 失败返回-1,错误信息存放于errno。
#include#include /* sockfd: socket文件描述符 backlog: 同时允许和服务端建立连接的客户端数量 */ int listen(int sockfd, int backlog);
成功返回0。失败返回-1,错误信息存放于errno。
使用cat /proc/sys/net/ipv4/tcp_max_syn_backlog可以查看系统默认的backlog。
如果客户端连接数达到backlog,新的客户端连接请求会被忽略。
#include#include /* sockfd:socket文件描述符 addr:传出参数,返回连接客户端地址信息,包含IP地址和端口号。如果传NULL,表示不关心客户端的地址 addrlen:传入传出参数,传入addr的大小,传出真正接收到地址结构体的大小 */ int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时没有收到客户端的请求,会一直处于阻塞状态。
成功返回一个新的 socket文件描述符,用于和客户端通信。失败返回-1,错误信息存放于errno。
include#include /* sockfd:socket文件描述符 addr:传入参数,指定服务器端地址信息,含端口号和IP地址 addrlen:传入参数,传入sizeof(addr)的大小 */ int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端需要调用connect()连接服务器,connect()和bind()的参数一致,区别在于bind()的参数是自己的地址,而connect()的参数是对方的地址。
成功返回0。失败返回-1,错误信息存放于errno。
这两个函数是socket tcp编程下的接收函数(recv)和发送函数(send)。
#includessize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
#include#include ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv和send函数和标准的read和write函数很类似,唯一的差别是第四个参数。
第四个参数是一个整型的标志位,我们可以以位或的形式包含系统允许的一系列标志,从而设置在这一次I/O的特性。
通常情况下被设置为0,实现普通read和write的功能。函数返回读取的字节个数。
#include#include /* src_addr: 接收到的struct socketaddr地址 addrlen: 接收到的struct socketaddr的大小的地址 */ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); /* dest_addr: 要发送的struct socketaddr地址 addrlen: 要发送的struct socketaddr的大小 */ ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
recvfrom和sendto函数和recv和send函数比较相似,只是前者是用于UDP协议下的,后者用于TCP协议下的。区别仅仅是多了最后两个参数。函数返回读取的字节个数。
程序功能:客户端发送数据,服务端将数据转换为大写后,发送回客户端。
#include#include #include #include #include #include #include #include #define SERV_PORT 2017 #define BACKLOG 10 int main(void) { int lfd, cfd, res, i; char buf[BUFSIZ], clie_ip[BUFSIZ]; struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; ssize_t n; /* 1.socket() */ lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd == -1) perror("socket error.."); /* 2.create struct sockaddr_in */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 3.bind() */ res = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if(res == -1) perror("bind error.."); /* 4.listen() */ res = listen(lfd, BACKLOG); if (res == -1) perror("listen error.."); /* 5.accept() */ clie_addr_len = sizeof(clie_addr); cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len); printf("client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip, sizeof(clie_ip)), ntohs(clie_addr.sin_port)); /* 6.recv() and send() */ while(1) { n = recv(cfd, buf, sizeof(buf), 0); if (n > 0) { printf("Receive msg: %s",buf); for(i = 0; i < n; i++) buf[i] = toupper(buf[i]); printf("Send msg: %s",buf); send(cfd, buf, strlen(buf), 0); memset(buf, 0, sizeof(buf)); } } /* 6.close() */ close(lfd); close(cfd); }
#include#include #include #include #include #include #include #define SERV_IP "127.0.0.1" #define SERV_PORT 2017 int main(void) { int cfd, res; struct sockaddr_in serv_addr; ssize_t n; char buf[BUFSIZ]; /* 1.socket() */ cfd = socket(AF_INET, SOCK_STREAM, 0); if (cfd == -1) perror("socket error.."); /* 2.create struct sockaddr_in */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); /* 3.connect() */ res = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if (res == -1) perror("connect error..\n"); /* 4.send() and recv() */ while (1) { printf("Send msg : "); fgets(buf, sizeof(buf), stdin); send(cfd, buf, strlen(buf), 0); memset(buf, 0, sizeof(buf)); n = recv(cfd, buf, sizeof(buf), 0); if( n > 0) printf("Receive msg: %s", buf); } /* 4.close() */ close(cfd); return 0; }
程序功能:客户端发送数据,服务端将数据转换为大写后,发送回客户端。
#include#include #include #include #include #include #include #include #define SERV_PORT 2017 #define BACKLOG 10 int main(void) { int lfd, res, i; char buf[BUFSIZ], clie_ip[BUFSIZ]; struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; ssize_t n; /* 1.socket() */ lfd = socket(AF_INET, SOCK_DGRAM, 0); if (lfd == -1) perror("socket error.."); /* 2.create struct sockaddr_in */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 3.bind() */ res = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if(res == -1) perror("bind error.."); /* 4.recvfrom() and sendto() */ while(1) { clie_addr_len = sizeof(clie_addr); n = recvfrom(lfd, buf, sizeof(buf), 0, (struct sockaddr *)&clie_addr, &clie_addr_len); if (n == -1) perror("recvfrom error.."); else if (n > 0) { printf("recvfrom ip : %s, port : %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip, sizeof(clie_ip)), ntohs(clie_addr.sin_port)); printf("Receive msg: %s",buf); for(i = 0; i < n; i++) buf[i] = toupper(buf[i]); printf("Send msg: %s",buf); n = sendto(lfd, buf, strlen(buf), 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr)); if (n == -1) perror("sendto error.."); memset(buf, 0, sizeof(buf)); } } /* 4.close() */ close(lfd); }
#include#include #include #include #include #include #include #define SERV_IP "127.0.0.1" #define SERV_PORT 2017 int main(void) { int cfd, res; struct sockaddr_in serv_addr; ssize_t n; char buf[BUFSIZ]; /* 1.socket() */ cfd = socket(AF_INET, SOCK_DGRAM, 0); if (cfd == -1) perror("socket error.."); /* 2.create struct sockaddr_in */ memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); /* 3.sendto() and recvfrom() */ while (fgets(buf, sizeof(buf), stdin) != NULL) { printf("Send msg : %s", buf); n = sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if (n == -1) perror("sendto error.."); n = recvfrom(cfd, buf, sizeof(buf), 0, NULL, 0); if( n == -1) perror("recvfrom error.."); printf("Receive msg: %s", buf); memset(buf, 0, sizeof(buf)); } /* 4.close() */ close(cfd); return 0; }