socket编程 字节序 在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,就是大于一个字节类型的数据在内存中的存放顺序。 对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
字节序的分类,如下: (1)主机字节序(小端,Little-Endian) 数据的低位字节存储到内存的低地址位,数据的高位字节存储到内存的高地址位。 我们使用的 PC 机,数据的存储默认使用的是小端。 (2)网络字节序(大端,Big-Endian) 数据的低位字节存储到内存的高地址位,数据的高位字节存储到内存的低地址位 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。 字节序,举例如下:
1 2 3 4 5 6 内存低地址位 内存的高地址位 ---------------------------> 小端:0x78 0x56 0x34 0x12 大端:0x12 0x34 0x56 0x78
Socket 提供了封装好的转换接口,方便程序员使用。 主机字节序->网络字节序的转换函数:htons
、htonl
; 网络字节序->主机字节序的转换函数:ntohs
、ntohl
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; uint32_t htonl (uint32_t hostlong) ; uint16_t ntohs (uint16_t netshort) uint32_t ntohl (uint32_t netlong) ;
IP地址转换 虽然 IP 地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述。 下面的函数描述了如何将一个字符串类型的 IP 地址进行大小端转换 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <arpa/inet.h> const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;
还有一组函数也能进程 IP 地址大小端的转换,但是只能处理 ipv4 的 ip 地址:
1 2 3 4 5 in_addr_t inet_addr (const char *cp) ;char * inet_ntoa (struct in_addr in) ;
scokaddr数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; }typedef unsigned short uint16_t ;typedef unsigned int uint32_t ;typedef uint16_t in_port_t ;typedef uint32_t in_addr_t ;typedef unsigned short int sa_family_t ;#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)) struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[sizeof (struct sockaddr) - sizeof (sin_family) - sizeof (in_port_t ) - sizeof (struct in_addr)]; };
套接字函数 使用套接字通信函数需要包含头文件 <arpa/inet.h>
,包含了这个头文件 <sys/socket.h>
就不用再包含。
socket 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int socket (int domain, int type, int protocol) ; 函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存, 网络通信是基于这个文件描述符来完成的。
bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
listen 1 2 3 4 5 6 7 8 9 10 11 12 13 int listen (int sockfd, int backlog) ;
accept 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞; 当检测到有新的客户端连接请求时,阻塞解除,新连接建立得到的返回值也是一个文件描述符, 基于这个文件描述符就可以和客户端通信。
connect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
接收函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t read (int sockfd, void *buf, size_t size) ;ssize_t recv (int sockfd, void *buf, size_t size, int flags) ;
如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达, 当数据到达后函数解除阻塞,开始接收数据, 当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞,函数直接返回 0。
发送函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ssize_t write (int fd, const void *buf, size_t len) ;ssize_t send (int fd, const void *buf, size_t len, int flags) ;
TCP通信流程 TCP 是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
面向连接:是一个双向连接,通过三次握手完成,断开连接通过四次挥手完成。 安全:tcp 通信过程中,会对发送的每一数据包都会进行校验,如果发现数据丢失,会自动重传。 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致。
服务端通信流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int lfd = socket ();bind ();listen ();int cfd = accept ();read (); write (); close ();
在 tcp 的服务器端,有两类文件描述符: (1)监听的文件描述符 只需要有一个。 不负责和客户端通信,负责检测客户端的连接请求,检测到之后调用 accept
就可以建立新的连接。 (2)通信的文件描述符 负责和建立连接的客户端通信。 如果有 N 个客户端和服务器建立了新的连接,通信的文件描述符就有 N 个,每个客户端和服务器都对应一个通信的文件描述符。
文件描述符对应的内存结构: (1)一个文件文件描述符对应两块内存, 一块内存是读缓冲区,一块内存是写缓冲区 (2)读数据:通过文件描述符将内存中的数据读出,这块内存称之为读缓冲区 (3)写数据:通过文件描述符将数据写入到某块内存中,这块内存称之为写缓冲区
监听的文件描述符: (1)客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中 (2)读缓冲区中有数据,说明有新的客户端连接 (3)调用 accept()
函数,这个函数会检测监听文件描述符的读缓冲区 如果检测不到数据,该函数阻塞; 如果检测到数据,解除阻塞,新的连接建立。
通信的文件描述符: (1)客户端和服务器端都有通信的文件描述符 (2)发送数据:调用函数 write()/send()
,数据进入到内核中 数据并没有被发送出去,而是将数据写入到了通信的文件描述符对应的写缓冲区中 内核检测到通信的文件描述符写缓冲区中有数据,内核会将数据发送到网络中 (3)接收数据:调用的函数 read()/recv()
,从内核读数据 数据如何进入到内核程序员不需要处理,数据进入到通信的文件描述符的读缓冲区中 数据进入到内核,必须使用通信的文件描述符,将数据从读缓冲区中读出
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main () { int fd = socket (AF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror ("socket" ); exit (-1 ); } struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons (9999 ); saddr.sin_addr.s_addr = INADDR_ANY; int ret = bind (fd, (struct sockaddr*) &saddr, sizeof (saddr)); if (ret == -1 ) { perror ("bind" ); exit (-1 ); } ret = listen (fd, 128 ); if (ret == -1 ) { perror ("listen" ); exit (-1 ); } struct sockaddr_in caddr; int addrlen = sizeof (caddr); int cfd = accept (fd, (struct sockaddr*) &caddr, &addrlen); if (cfd == -1 ) { perror ("accept" ); exit (-1 ); } char ip[32 ]; inet_ntop (AF_INET, &caddr.sin_addr.s_addr, ip, sizeof (ip)); unsigned short port = ntohs (caddr.sin_port); printf ("client ip: %s, port: %d\n" , ip, port); while (1 ) { char buff[1024 ]; memset (buff, 0 , sizeof (buff)); int len = read (cfd, buff, sizeof (buff)); if (len > 0 ) { printf ("read client data : %s\n" , buff); } else if (len == 0 ) { printf ("client closed...\n" ); break ; } else { perror ("read" ); exit (-1 ); } char * data = "hello, i am server" ; write (cfd, data, strlen (data)); } close (fd); close (cfd); return 0 ; }
客户端通信流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int cfd = socket ();connect ();read (); write (); close ();
注意:在单线程的情况下客户端通信的文件描述符有一个,没有监听的文件描述符。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main () { int fd = socket (AF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror ("socket" ); exit (-1 ); } struct sockaddr_in saddr; saddr.sin_family = AF_INET; saddr.sin_port = htons (9999 ); inet_pton (AF_INET, "192.168.3.194" , &saddr.sin_addr.s_addr); int ret = connect (fd, (struct sockaddr*) &saddr, sizeof (saddr)); if (ret == -1 ) { perror ("connect" ); exit (-1 ); } while (1 ) { char buff[1024 ]; sprintf (buff, "hello, i am client" ); write (fd, buff, strlen (buff)); memset (buff, 0 , sizeof (buff)); int len = read (fd, buff, sizeof (buff)); if (len > 0 ) { printf ("read server data : %s\n" , buff); } else if (len == 0 ) { printf ("server closed...\n" ); break ; } else { perror ("read" ); exit (-1 ); } sleep (1 ); } close (fd); return 0 ; }
Window 套接字通信 在 Window 中也提供了套接字通信的 API,这些 API 函数与 Linux 平台的 API 函数几乎相同。 下面看一下这些 Windows 平台的套接字函数:
初始化套接字环境 使用 Windows 中的套接字函数需要额外包含对应的头文件以及加载响应的动态库
1 2 3 4 #include <winsock2.h> ws2_32.dll
Windows 中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源。
1 2 3 4 5 6 7 8 9 10 11 12 WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);
注销 Winsock 相关库,函数调用成功返回 0,失败返回 SOCKET_ERROR。
使用示例:
1 2 3 4 5 6 7 8 WSAData wsa;WSAStartup (MAKEWORD (2 , 2 ), &wsa);WSACleanup ();
套接字通信函数 基于 Linux 的套接字通信流程是最全面的一套通信流程,如果是在某个框架中进行套接字通信,通信流程只会更简单, 直接使用 window 的套接字 api 进行套接字通信,和 Linux 平台上的通信流程完全相同。
结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 typedef struct in_addr { union { struct { unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b; struct { unsigned short s_w1, s_w2;} S_un_w; unsigned long S_addr; } S_un; } IN_ADDR;struct sockaddr_in { short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero[8 ]; };typedef unsigned short uint16_t ;typedef unsigned int uint32_t ;typedef uint16_t in_port_t ;typedef uint32_t in_addr_t ;typedef unsigned short int sa_family_t ;struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[sizeof (struct sockaddr) - sizeof (sin_family) - sizeof (in_port_t ) - sizeof (struct in_addr)]; };
大小端转换函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 u_short htons (u_short hostshort) ;u_long htonl (u_long hostlong) ;u_short ntohs (u_short netshort) ;u_long ntohl (u_long netlong) ;inet_ntop (); inet_pton ();unsigned long inet_addr (const char FAR * cp) ; in_addr_t inet_addr (const char *cp) ; char * inet_ntoa (struct in_addr in) ;
套接字函数 window 的 api 中套接字对应的类型是 SOCKET 类型,linux中是 int 类型,本质是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 SOCKET socket (int af, int type, int protocal) ;int bind (SOCKET s, const struct sockaddr FAR* name, int namelen) ;int listen (SOCKET s, int backlog) ;SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen) ;int connect (SOCKET s, const struct sockaddr FAR* name, int namelen) ; ::connect (sock, (struct sockaddr*)&addr, sizeof (addr));int recv (SOCKET s, char FAR* buf, int len, int flags) ;int send (SOCKET s, const char FAR * buf, int len, int flags) ;int closesocket (SOCKET s) ; int recvfrom (SOCKET s, char FAR *buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen) ;int sendto (SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen) ;
参考资料 https://subingwen.cn/linux/socket