1、理解网络编程和套接字
网络编程就是编写程序使两台连网的计算机相互交互数据。
1.1构建接电话套接字
套接字大致分为两种,其中,先要讨论的TCP套接字可以比喻成电话机。实际上,电话机也是通过电话网完成语言数据交换的。
#include <sys/socket.h> /* Create a new socket of type TYPE in domain DOMAIN, using protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically. Returns a file descriptor for the new socket, or -1 for errors. */ extern int socket (int __domain, int __type, int __protocol) __THROW;
成功时返回文件描述符,失败时返回-1
#include <sys/socket.h> /* Give the socket FD the local address ADDR (which is LEN bytes long). */ extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
成功时返回0,失败时返回-1
#include <sys/socket.h> /* Prepare to accept connections on socket FD. N connection requests will be queued before further requests are refused. Returns 0 on success, -1 for errors. */ extern int listen (int __fd, int __n) __THROW;
成功时返回0,失败时返回-1
#include <sys/socket.h> /* Await a connection on socket FD. When a connection arrives, open a new socket to communicate with it, set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting peer and *ADDR_LEN to the address's actual length, and return the new socket's descriptor, or -1 for errors. This function is a cancellation point and therefore not marked with __THROW. */ extern int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
成功时返回文件描述符,失败时返回-1
网络编程中接受连接请求的套接字过程可整理如下:
第一步:调用socket函数穿件套接字
第二步:调用bind函数分配IP地址和端口号
第三步:调用listen函数转为可接收请求状态
第四步:调用accept函数受理连接请求。
1.2编写“Hello World”服务器端
1 // 2 // Created by starry on 19-3-6. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <unistd.h> 9 #include <arpa/inet.h> 10 #include <sys/socket.h> 11 12 void error_handling(char *messahe); 13 14 int main(int argc, char *argv[]) { 15 int serv_sock; 16 int clnt_sock; 17 18 struct sockaddr_in serv_addr; 19 struct sockaddr_in clnt_addr; 20 socklen_t clnt_addr_size; 21 22 char message[] = "Hello World!"; 23 24 if(argc != 2) { 25 printf("Usage : %s <port> ",argv[0]); 26 exit(1); 27 } 28 serv_sock = socket(PF_INET, SOCK_STREAM, 0); 29 if(serv_sock == -1) 30 error_handling("socket() error"); 31 32 memset(&serv_addr, 0, sizeof(serv_addr)); 33 serv_addr.sin_family = AF_INET; 34 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 35 serv_addr.sin_port = htons(atoi(argv[1])); 36 37 if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1) 38 error_handling("bind() error"); 39 40 if(listen(serv_sock, 5) == -1) 41 error_handling("listen() error"); 42 43 clnt_addr_size = sizeof(clnt_addr); 44 clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); 45 if(clnt_sock == -1) 46 error_handling("accept() error"); 47 48 write(clnt_sock, message, sizeof(message)); 49 close(clnt_sock); 50 close(serv_sock); 51 return 0; 52 } 53 54 void error_handling(char *message) { 55 fputs(message, stderr); 56 fputc(' ', stderr); 57 exit(1); 58 }
第28行:调用socket函数穿件套接字。
第37行:调用bind函数分配IP地址和端口号。
第40行:调用listen函数将套接字转为可连接接收状态。
第44行:调用accept函数受理连接请求。如果在没有连接请求的情况下调用该函数,则步会返回,直到有连接请求为止。
第48行:调用write函数向客户端传输数据。
1.3构建打电话套接字
#include <sys/socket.h> /* Open a connection on socket FD to peer at ADDR (which LEN bytes long). For connectionless socket types, just set the default address to send to and the only address from which to accept transmissions. Return 0 on success, -1 for errors. This function is a cancellation point and therefore not marked with __THROW. */ extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
成功时返回0,失败时返回-1.
客户端程序,第一,调用socket函数和connect函数;第二,与服务器共同运行以发字符串数据。
1 // 2 // Created by starry on 19-3-6. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <unistd.h> 9 #include <arpa/inet.h> 10 #include <sys/socket.h> 11 12 void error_handling(char *message); 13 14 15 int main(int argc, char *argv[]) { 16 int sock; 17 struct sockaddr_in serv_addr; 18 char message[30]; 19 int str_len; 20 21 if(argc != 3) { 22 printf("Usage : %s <IP> <port> ",argv[0]); 23 exit(1); 24 } 25 26 sock = socket(PF_INET, SOCK_STREAM, 0); 27 if(sock == -1) 28 error_handling("socket() error"); 29 30 memset(&serv_addr, 0, sizeof(serv_addr)); 31 serv_addr.sin_family = AF_INET; 32 serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 33 serv_addr.sin_port = htons(atoi(argv[2])); 34 35 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) 36 error_handling("connect() error"); 37 38 str_len = read(sock, message, sizeof(message)-1); 39 if(str_len == -1) 40 error_handling("read() error"); 41 42 printf("Message from server: %s ", message); 43 close(sock); 44 return 0; 45 } 46 47 void error_handling(char *message) { 48 fputs(message, stderr); 49 fputc(' ', stderr); 50 exit(1); 51 }
第26行:创建套接字,但此时套接字并不马上分为服务器端和客户端。如果紧接着调用bind、listen函数,将成为服务器端套接字;如果调用connect函数,将成为客户端套接字。
第35行:调用connect函数向服务器端发送连接请求。
1.4基于Linux的文件操作
对Linux而言,socket操作与文件操作没有区别,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。window则与Linux不同,是要区分socket和文件的,因此在window中需要调用特殊的数据传输相关函数。
默认文件描述符
0 标准输入
1 标准输出
2 标准错误
文件和套接字一般经过创建过程才会被分配文件描述符。
打开文件
#include <fcntl.h> extern int open (const char *__file, int __oflag, ...) __nonnull ((1));
__file:文件名的字符串地址
__oflag:文件打开模式信息
文件打开模式
O_CREAT 必要时创建文件
O_TRUNC 删除全部现有数据
O_APPEND 维持现有数据,保存到其后面
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
关闭文件
#include <unistd.h> /* Close the file descriptor FD. This function is a cancellation point and therefore not marked with __THROW. */ extern int close (int __fd);
__fd:文件描述符
将数据写入文件
#include <unistd.h> /* Write N bytes of BUF to FD. Return the number written, or -1. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
__fd:显示数据传输对象的文件描述符
__buf:保存要传输数据的缓冲地址值
__n:要传输数据的字节数
1 // 2 // Created by starry on 19-3-6. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <fcntl.h> 8 #include <unistd.h> 9 void error_handling(char* message); 10 11 int main(void) { 12 13 int fd; 14 char buf[] = "Let's go! "; 15 16 fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC); 17 if(fd == -1) 18 error_handling("open() error!"); 19 printf("file descriptor: %d ",fd); 20 21 if(write(fd, buf, sizeof(buf)) == -1) 22 error_handling("write() error"); 23 close(fd); 24 return 0; 25 } 26 void error_handling(char *message) { 27 fputs(message, stderr); 28 fputc(' ', stderr); 29 exit(1); 30 }
读取文件中的数据
#include <unistd.h> /* Read NBYTES into BUF from FD. Return the number read, -1 for errors or 0 for EOF. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;
__fd:显示数据接收对象的文件描述符
__buf:要保存接收数据的缓冲地址值
__nbytes:要接收数据的最大字节数
1 // 2 // Created by Starry on 2019/3/13. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <fcntl.h> 8 #include <unistd.h> 9 #define BUF_SIZE 100 10 void error_handling(const char* message); 11 12 int main(void) { 13 int fd; 14 char buf[BUF_SIZE]; 15 16 fd = open("data.txt", O_RDONLY); 17 if(fd == -1) 18 error_handling("open() error"); 19 printf("file descriptor:%d ",fd); 20 21 if(read(fd, buf, sizeof(buf)) == -1) 22 error_handling("read() error!"); 23 printf("file data:%s", buf); 24 close(fd); 25 return 0; 26 } 27 28 void error_handling(const char *message) { 29 fputs(message, stderr); 30 fputc(' ', stderr); 31 exit(1); 32 }
2套接字类型与协议设置
协议就是为了完成数据交换而定好的约定
2.1套接字协议及其数据传输特性
创建套接字
extern int socket (int __domain, int __type, int __protocol) __THROW;
__domain:套接字中使用的协议族信息。
__type:套接字数据传输类型信息。
__protocol:计算机通信中使用的协议信息。
协议族:
sys/socket.h中声明的协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族
套接字类型
类型1:面向连接的套接字SOCK_STREAM TCP
1、传输过程中数据不会消失
2、按序传输数据
3、传输的数据不存在数据边界
类型2:面向消息的套接字SOCK_DGRAM UDP
1、强调快速传输而非传输顺序
2、传输的数据可能丢失也可能损毁
3、传输的数据有数据边界
4、限制每次传输的数据大小
SOCK_STREAM传输数据不存在数据边界用下面的代码可以验证
1 // 2 // Created by Starry on 2019/3/15. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <unistd.h> 9 #include <arpa/inet.h> 10 #include <sys/socket.h> 11 12 void error_handling(char *message); 13 14 15 int main(int argc, char *argv[]) { 16 int sock; 17 struct sockaddr_in serv_addr; 18 char message[30]; 19 int str_len = 0; 20 int idx = 0, read_len = 0; 21 22 if(argc != 3) { 23 printf("Usage : %s <IP> <port> ",argv[0]); 24 exit(1); 25 } 26 27 sock = socket(PF_INET, SOCK_STREAM, 0); 28 if(sock == -1) 29 error_handling("socket() error"); 30 31 memset(&serv_addr, 0, sizeof(serv_addr)); 32 serv_addr.sin_family = AF_INET; 33 serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 34 serv_addr.sin_port = htons(atoi(argv[2])); 35 36 if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) 37 error_handling("connect() error"); 38 39 while(read_len=read(sock, &message[idx++], 1)) { 40 if(read_len == -1) 41 error_handling("read() error!"); 42 str_len += read_len; 43 } 44 45 printf("Message from server: %s ", message); 46 printf("Funciton read call count: %d ",str_len); 47 close(sock); 48 return 0; 49 } 50 51 void error_handling(char *message) { 52 fputs(message, stderr); 53 fputc(' ', stderr); 54 exit(1); 55 }
服务的只发送了一个字符串,但客户端每次只接收一个字节,直到接收完为止。
SOCK_DGRAM的话一次发送只能一次接收。
3地址族与数据序列
3.1地址信息的表示
表示IPv4地址的结构体
/* Structure describing an Internet socket address. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };
/* Internet address. */ typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; };
sin_表示sin_family成员,设置地址协议族的
sin_port:该成员保存16位端口号,重点在于,它以网络字节序保存
sin_addr:该成员保存32位IP地址信息,且也以网络字节序保存。
sin_zero:无特殊意义,只是为使结构体sockaddr_in的大小与sockaddr结构体保存一致而插入的成员,必须填充0,否则无法得到想要的结果。
3.2网络字节序与地址变换
CPU向内存保存数据的方法有2种。
大端序:高位字节存放到低位地址。
小段序:高位字节存放到高位地址。
主流的CPU都是以小端序方式保存数据的。而在网络传输数据时约定统一方式,用大端序。所以要把数据转化成大端序格式在进行网络传输。因此,所有计算机接收数据时应识别该数据是网络字节序格式,小端序系统传输数据时应转化为大端序排序方式。
字节序转换
extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__)); extern uint16_t ntohs (uint16_t __netshort) __THROW __attribute__ ((__const__)); extern uint32_t htonl (uint32_t __hostlong) __THROW __attribute__ ((__const__)); extern uint16_t htons (uint16_t __hostshort) __THROW __attribute__ ((__const__));
h代表主机字节序,n代表网络字节序。s指的是short,l指的是long(Linux中long类型占4个字节)
1 // 2 // Created by Starry on 2019/3/15. 3 // 4 5 #include <stdio.h> 6 #include <arpa/inet.h> 7 8 int main(int argc, char* argv[]) { 9 unsigned short host_port = 0x1234; 10 unsigned short net_port; 11 unsigned long host_addar = 0x12345678; 12 unsigned long net_addr; 13 14 net_port = htons(host_port); 15 net_addr = htonl(host_addar); 16 17 printf("Host ordred post:%#x ",host_port); 18 printf("Network ordred post:%#x ",net_port); 19 printf("Host ordred address:%#x ",host_addar); 20 printf("Network ordred address:%#x ",net_addr); 21 22 return 0; 23 }
3.3网络地址的初始化与分配
将字符串信息转换为网络字节序的整数型,sockaddr_in中保存地址信息的成员32位整数型,因此为了分配IP地址,需要将其表示为32位整数型数据。
#include <arpa/inet.h> /* Convert Internet host address from numbers-and-dots notation in CP into binary data in network byte order. */ extern in_addr_t inet_addr (const char *__cp) __THROW;
传入字符串的IP信息,成功时返回32位大端序整数型值,失败时返回INADDR_NONE。
1 // 2 // Created by starry on 19-3-16. 3 // 4 5 #include <stdio.h> 6 #include <arpa/inet.h> 7 8 int main(int argc, char* argv[]) { 9 char *addr1 = "1.2.3.4"; 10 char *addr2 = "1.2.3.256"; 11 12 unsigned long conv_addr = inet_addr(addr1); 13 if (conv_addr == INADDR_NONE) 14 printf("Error occured! "); 15 else 16 printf("Network ordred integer addr: %#1x ", conv_addr); 17 18 conv_addr = inet_addr(addr2); 19 if (conv_addr == INADDR_NONE) 20 printf("Error occured! "); 21 else 22 printf("Network ordred integer addr: %#1x ", conv_addr); 23 return 0; 24 }
inet_addr函数不仅可以把IP地址转换成32位整数型,而且可以检测无效的IP地址。
#include <arpa/inet.h> /* Convert Internet host address from numbers-and-dots notation in CP into binary data and store the result in the structure INP. */ extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;
inet_aton也用相同的功能,不过是将值存储在__inp里,成功时返回1,失败时返回0
1 // 2 // Created by starry on 19-3-16. 3 // 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 #include <arpa/inet.h> 8 void error_handling(char *message); 9 10 int main(int argc, char* argv[]) { 11 char *addr = "127.232.124.79"; 12 struct sockaddr_in addr_inet; 13 14 if(!inet_aton(addr, &addr_inet.sin_addr)) 15 error_handling("Conversion error"); 16 else 17 printf("Network ordred integer addr: %#x ", 18 addr_inet.sin_addr.s_addr); 19 return 0; 20 } 21 void error_handling(char *message) { 22 fputs(message, stderr); 23 fputc(' ', stderr); 24 exit(1); 25 }
#include <arpa/inet.h> /* Convert Internet number in IN to ASCII representation. The return value is a pointer to an internal array containing the string. */ extern char *inet_ntoa (struct in_addr __in) __THROW;
inet_ntoa的功能相反,将整数型IP转换为字符串并返回
1 // 2 // Created by starry on 19-3-16. 3 // 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <arpa/inet.h> 8 9 int main(int argc, char* argv[]) { 10 struct sockaddr_in addr1, addr2; 11 char *str_ptr; 12 char str_arr[20]; 13 addr1.sin_addr.s_addr = htonl(0x1020304); 14 addr2.sin_addr.s_addr = htonl(0x1010101); 15 16 str_ptr = inet_ntoa(addr1.sin_addr); 17 strcpy(str_arr,str_ptr); 18 printf("Doctted-Decimal notation1: %s ",str_ptr); 19 20 inet_ntoa(addr2.sin_addr); 21 printf("Doctted-Decimal notation2: %s ",str_ptr); 22 printf("Doctted-Decimal notation3: %s ",str_arr); 23 24 return 0; 25 }
不过返回的字符串需要复制到其他字符数组里,不然下一次调用时会覆盖掉。
每次创建服务器端套接字都要输入IP地址,利用常熟INADDR_ANY分配服务器端的IP地址。