注:博主只关注编程实现的方面以及linux部分,部分网络原理讲解和windows实现部分跳过
第1章 理解网络编程和套接字
1.1 理解网络编程和套接字
Q:网络编程中接受连接请求的套接字创建过程?
1.调用 socket 函数创建套接字
#include <sys/socket.h> int socket(int domain, int type, int protocol); // domain:套接字中使用的协议族(Protocol Family)信息 // type:套接字数据传输类型信息 // protocol:计算机间通信中使用的协议信息 // 成功时返回文件描述符,失败时返回-1
2.调用 bind 函数分配IP地址和端口号
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); // sockfd:要分配地址信息(IP地址和端口号)的套接字文件描述符 // myaddr:存有地址信息的结构体变量地址值 // addrlen:第二个结构体变量的长度 // 成功时返回0,失败时返回-1
3.调用 listen 函数转换为可接受请求状态
#include <sys/socket.h> int listen(int sockfd, int backlog); // sock:希望进入等待连接请求状态的套接字文件描述符, // 传递的描述符套接字参数成为服务端套接字(监听套接字) // backlog:连接请求等待队列的长度,若为5,则队列长度为5, // 表示最多使5个连接请求进入队列 // 成功时返回0,失败时返回-1
4.调用 accept 函数受理套接字请求
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // sockfd: 服务端套接字的文件描述符 // addr: 保存发起连接请求的客户端地址信息的变量地址值 // 调用函数后向传递来的地址变量参数填充客户端地址信息 // addrlen: 第二个参数addr结构体的长度,但是存放有长度的变量地址。 // 函数调用完成后,该变量即被填入客户端地址长度 // 成功时返回文件描述符,失败时返回-1
accept 函数受理连接请求队列中待处理的客户端连接请求。函数调用成功时,accept 函数内部将产生用于数据I/O的套接字,并返回其文件描述符
需要强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接
Q:请求连接客户端程序的套接字的创建过程?
1.调用 socket 函数 和 connect 函数
#include <sys/socket.h> int conncet(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen); // sockfd: 客户端套接字文件描述符 // servaddr: 保存目标服务器端地址信息的变量地址值 // addrlen: 以字节为单位传递给第二个结构体参数 servaddr 的变量地址长度 // 成功时返回0,失败时返回-1
客户端调用 connect 函数候,发生以下函数之一才会返回(完成函数调用):
- 服务器接受连接请求
- 发生断网等异常情况而中断连接请求
注意:「接受连接」并不代表服务端调用accept函数,其实是服务端把连接请求信息记录到等待队列。因此 connect 函数返回后并不立即进行数据交换
2.与服务端共同运行以收发字符串数据
Q:Hello World服务端和客户端的例子
服务端hello_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello World!"; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } // 调用 socket 函数创建套接字 serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) { error_handling("socket() error"); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); // 调用 bind 函数分配ip地址和端口号 if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) { error_handling("bind() error"); } // 调用 listen 函数将套接字转为可接受连接状态 if (listen(serv_sock, 5) == -1) { error_handling("listen() error"); } clnt_addr_size = sizeof(clnt_addr); // 调用 accept 函数受理连接请求。 // 如果在没有连接请求的情况下调用该函数, // 则不会返回,直到有连接请求为止 clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if (clnt_sock == -1) { error_handling("accept() error"); } // write 函数用于传输数据,若程序经过 accept 这一行执行到本行, // 则说明已经有了连接请求 write(clnt_sock, message, sizeof(message)); close(clnt_sock); close(serv_sock); return 0; }
客户端hello_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int sock; struct sockaddr_in serv_addr; char message[30]; int str_len; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } // 创建套接字,此时套接字并不马上分为服务端和客户端 // 如果紧接着调用 bind,listen 函数,将成为服务器套接字 // 如果调用 connect 函数,将成为客户端套接字 sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); // 调用 connect 函数向服务器发送连接请求 if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { error_handling("connect() error!"); } str_len = read(sock, message, sizeof(message) - 1); if (str_len == -1) { error_handling("read() error!"); } printf("Message from server : %s \n", message); close(sock); return 0; }
编译运行
服务端
shiqi@inspiron:~/network$ gcc hello_server.c -o hserver shiqi@inspiron:~/network$ ./hserver 9190
客户端
shiqi@inspiron:~/network$ gcc hello_client.c -o hclient shiqi@inspiron:~/network$ ./hclient 127.0.0.1 9190 Message from server : Hello World!
1.2 基于 Linux 的文件操作
Q:分配给标准输入输出及标准错误的文件描述符
- 0:标准输入(Standard Input)
- 1:标准输出(Standard Output)
- 2:标准错误(Standard Error)
Q:打开文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char * path, int flag); // path:文件名的字符串地址 // flag:文件打开模式(文件特性信息) // 成功时返回文件描述符,失败时返回-1
文件打开模式,如需传递多个参数,可通过位或运算符组合传递
O_CREAT:必要时创建文件
O_TRUNC:删除全部现有数据
O_APPEND:维持现有数据,保存到其后面
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读写打开
Q:关闭文件
#include <unistd.h> int close(int fd); // fd:需要关闭的文件或套接字的文件描述符 // 成功时返回 0 ,失败时返回 -1
Q:将数据写入文件
#include <unistd.h> ssize_t write(int fd, const void * buf, size_t nbytes); // fd:显示数据传输对象的文件描述符 // buf:保存要传输数据的缓冲值地址 // nbytes:要传输数据的字节数 // 成功时返回写入的字节数 ,失败时返回 -1
在此函数的定义中,size_t 是通过 typedef 声明的 unsigned int 类型。对 ssize_t 来说,ssize_t 前面多加的 s 代表 signed ,即 ssize_t 是通过 typedef 声明的 signed int 类型
Q:创建新文件并保存数据示例low_open.c
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main() { char buf[] = "Let's go!\n"; int fd; // O_CREAT | O_WRONLY | O_TRUNC 是文件打开模式,将创建新文件,并且只能写 // 如存在 data.txt 文件,则清空文件中的全部数据 fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC); if (fd == -1) { error_handling("open() error!"); } printf("file descriptor: %d \n", fd); // 向对应 fd 中保存的文件描述符的文件传输 buf 中保存的数据 if (write(fd, buf, sizeof(buf)) == -1) { error_handling("write() error!"); } close(fd); return 0; }
shiqi@inspiron:~/network$ gcc low_open.c -o lopen shiqi@inspiron:~/network$ ./lopen file descriptor: 3 shiqi@inspiron:~/network$ cat data.txt Let's go!
Q:读取文件中的数据
#include <unistd.h> ssize_t read(int fd, void *buf, size_t nbytes); // fd: 显示数据接收对象的文件描述符 // buf: 要保存接收的数据的缓冲地址值。 // nbytes: 要接收数据的最大字节数 // 成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回 -1
Q:通过read()读取data.txt中的数据示例low_read.c
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #define BUF_SIZE 100 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main() { char buf[BUF_SIZE]; int fd = open("data.txt", O_RDONLY); if (fd == -1) { error_handling("open() error!"); } printf("file descriptor: %d \n", fd); if (read(fd, buf, sizeof(buf)) == -1) { error_handling("read() error!"); } printf("file data: %s", buf); close(fd); return 0; }
shiqi@inspiron:~/network$ gcc low_read.c -o lread shiqi@inspiron:~/network$ ./lread file descriptor: 3 file data: Let's go!
Q:同时创建文件和套接字,并用整数型态比较返回的文件描述符的值,程序示例fd_seri.c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/socket.h> int main() { int fd1 = socket(PF_INET, SOCK_STREAM, 0); int fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC); int fd3 = socket(PF_INET, SOCK_DGRAM, 0); printf("fd1: %d\n", fd1); printf("fd2: %d\n", fd2); printf("fd3: %d\n", fd3); close(fd1); close(fd2); close(fd3); return 0; }
shiqi@inspiron:~/network$ gcc fd_seri.c -o fds shiqi@inspiron:~/network$ ./fds fd1: 3 fd2: 4 fd3: 5
1.3 基于 Windows 平台的实现
1.4 基于 Windows 的套接字相关函数及示例
第 2 章 套接字类型与协议设置
2.1 套接字协议及数据传输特性
Q:头文件 sys/socket.h 中声明的协议族有哪些
- 通过 socket 函数的第一个参数传递套接字中使用的协议分类信息,即协议族(Protocol Family)
- PF_INET:IPV4互联网协议族
- PF_INET6:IPV6互联网协议族
- PF_LOCAL:本地通信 Unix 协议族
- PF_PACKET:底层套接字的协议族
- PF_IPX:IPX Novel 协议族
- 套接字中实际采用的最终的协议信息是通过 socket 函数的第三个参数传递的。在指定的协议族范围内通过第一个参数决定第三个参数
Q:套接字类型1:面向连接的套接字(SOCK_STREAM)的3个特点是什么
- 特点
- 传输过程中数据不会消失
- 按序传输数据
- 传输的数据不存在数据边界(Boundary)
- 收发数据的套接字内部有缓冲(buffer),即字节数组。通过套接字传输的数据将保存到该数组。因此收到数据并不意味着马上调用 read 函数。只要不超过数组容量,则有可能在数据填充满缓冲后通过一次或多次 read 函数调用读取
- 在面向连接的套接字中,read、write 函数的调用次数无太大意义,即为不存在数据边界
- 面向连接的套接字可总结为:可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字
- 特点
Q:套接字类型2:面向消息的套接字(SOCK_DGRAM)的4个特点是什么
- 特点
- 强调快速传输而非传输有序
- 传输的数据可能丢失也可能损毁
- 传输的数据有边界
- 限制每次传输数据的大小
- 面向消息的套接字比面向连接的套接字具有更快的传输速度,但无法避免数据丢失或损毁。另外,每次传输的数据大小具有一定限制,并存在数据边界。存在数据边界意味着接收数据的次数应和传输次数相同
- 面向消息的套接字特性总结:不可靠的、不按序传递的、以数据的高速传输为目的的套接字
- 特点
Q:socket 函数中,如何用第三个参数选择最终的协议
- socket 函数的第三个参数决定最终采用的协议。因同一协议族中存在多个数据传输方式相同的协议,需要通过第三个参数指定具体的协议
- 创建IPv4协议族(PF_INET)中面向连接的套接字(SOCK_STREAM),满足这两个条件的协议为 IPPROTO_TCP,即TCP套接字
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
- 创建IPv4协议族(PF_INET)中面向消息的套接字(SOCK_DGRAM),满足这两个条件的协议为 IPPROTO_UDP,即UDP套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)
Q:面向连接的套接字:TCP套接字示例
服务端tcp_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello World!"; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } // 调用 socket 函数创建套接字 serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) { error_handling("socket() error"); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); // 调用 bind 函数分配ip地址和端口号 if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) { error_handling("bind() error"); } // 调用 listen 函数将套接字转为可接受连接状态 if (listen(serv_sock, 5) == -1) { error_handling("listen() error"); } clnt_addr_size = sizeof(clnt_addr); // 调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数, // 则不会返回,直到有连接请求为止 clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if (clnt_sock == -1) { error_handling("accept() error"); } // write 函数用于传输数据,若程序经过 accept 这一行执行到本行, // 则说明已经有了连接请求 write(clnt_sock, message, sizeof(message)); close(clnt_sock); close(serv_sock); return 0; }
客户端tcp_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int sock; struct sockaddr_in serv_addr; char message[30]; int str_len = 0; int idx = 0, read_len = 0; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } // 创建TCP套接字,此时套接字并不马上分为服务端和客户端 // 如果紧接着调用 bind,listen 函数,将成为服务器套接字 // 如果调用 connect 函数,将成为客户端套接字 // // 若前两个参数使用PF_INET 和 SOCK_STREAM, // 则可以省略第三个参数 IPPROTO_TCP sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); // 调用 connect 函数向服务器发送连接请求 if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { error_handling("connect() error!"); } // while循环中反复调用read函数,每次读取1个字节。 // 如果read返回0,即文件末尾,则循环结束 while (read_len = read(sock, &message[idx++], 1)) { if (str_len == -1) { error_handling("read() error!"); } str_len += read_len; } printf("Message from server : %s \n", message); printf("Function read call count : %d \n", str_len); close(sock); return 0; }
编译运行
服务端
shiqi@inspiron:~/network$ gcc tcp_server.c -o hserver shiqi@inspiron:~/network$ ./hserver 9191
客户端
shiqi@inspiron:~/network$ gcc tcp_client.c -o hclient shiqi@inspiron:~/network$ ./hclient 127.0.0.1 9191 Message from server : Hello World! Function read call count : 13
第 3 章 地址族与数据序列
3.1 分配给套接字的 IP 地址与端口号
3.2 地址信息的表示
Q: POSIX中定义了哪些数据类型
Q:表示IPv4地址的结构体 sockaddr_in 和 in_addr
struct sockaddr_in { sa_family_t sin_family; // 地址族(Address Family) uint16_t sin_port; // 16位TCP/UDP端口号 struct in_addr sin_addr; // 32位IP地址 char sin_zero[8]; // 不使用 }; struct in_addr { in_addr_t s_addr; // 32位IPv4地址,同uint32_t };
sin_family:指定每种协议族适用的地址族(Address Family)
- AF_INET:IPV4用的地址族
- AF_INET6:IPV6用的地址族
- AF_LOCAL:本地通信中采用的 Unix 协议的地址族
sin_port
- 保持16位端口号,以网络字节序保存
sin_addr
- 保存32位IP地址信息,且以网络字节序保存,可同时观察结构体in_addr,其中声明为uint32_t,只需当作32位整数型即可
sin_zero
无特殊含义。只是为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必需填充为0,否则无法得到想要的结果
sockaddr_in结构体变量地址值将以如下方式传递给bind函数
struct sockaddr_in serv_addr; ... if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { error_handling("bind() error"); }
此处 bind 第二个参数期望得到的是 sockaddr 结构体变量的地址值,包括地址族、端口号、IP地址等
struct sockaddr { sa_family_t sin_family; // 地址族(Address Family) char sa_data[14]; // 地址信息 }
该结构体成员 sa_data 保存的地址信息中需包含IP地址和端口号,剩余部分应填充0。而这对于包含地址的信息非常麻烦,所以有sockaddr_in结构体,最后转换为sockaddr 型的结构体变量,再传递给 bind 函数
3.3 网络字节序与地址变换
Q:CPU向内存保存数据的2种方式
- 大端序(Big Endian):高位字节存放到低位地址
- 小端序(Little Endian):高位字节存放到高位地址
- 网络传输数据时约定统一为大端序,即网络字节序(Network Byte Order)
Q:字节序转换(Endian Conversions)函数
#include <netinet/in.h> unsigned short htons(unsigned short); unsigned short ntohs(unsigned short); unsigned long htonl(unsigned long); unsigned long ntohl(unsigned long);
h:主机(host)字节序
n:主机(host)字节序
s:2个字节short,用于端口号转换
l:4个字节long,用于IP地址转换
Q:字节序转换示例程序endian_conv.c
#include <stdio.h> #include <arpa/inet.h> int main() { unsigned short host_port = 0x1234; unsigned short net_port = htons(host_port); unsigned long host_addr = 0x12345678; unsigned long net_addr = htonl(host_addr); printf("Host ordered port: %#x \n", host_port); printf("Network ordered port: %#x \n", net_port); printf("Host ordered address: %#lx \n", host_addr); printf("Network ordered address: %#lx \n", net_addr); return 0; }
shiqi@inspiron:~/network$ gcc endian_conv.c -o conv shiqi@inspiron:~/network$ ./conv Host ordered port: 0x1234 Network ordered port: 0x3412 Host ordered address: 0x12345678 Network ordered address: 0x78563412
3.4 网络地址的初始化与分配
Q:将字符串形式的IP地址转换成32位整数型数据,并同时进行网络字节序转换的inet_addr
#include <arpa/inet.h> in_addr_t inet_addr(const char * string); // 成功时返回32位大端序整数型值,失败时返回INADDR_NONE
示例inet_addr.c
#include <stdio.h> #include <arpa/inet.h> int main() { char * addr1 = "1.2.3.4"; char * addr2 = "1.2.3.256"; unsigned long conv_addr = inet_addr(addr1); if (conv_addr == INADDR_NONE) { printf("Error occured!\n"); } else { printf("Network ordered integer addr: %#lx \n", conv_addr); } conv_addr = inet_addr(addr2); if (conv_addr == INADDR_NONE) { printf("Error occured!\n"); } else { printf("Network ordered integer addr: %#lx \n", conv_addr); } return 0; }
shiqi@inspiron:~/network$ gcc inet_addr.c -o addr shiqi@inspiron:~/network$ ./addr Network ordered integer addr: 0x4030201 Error occured!
Q:利用了in_addr结构体将字符串形式IP地址转换为32位网络字节序整数返回的inet_aton函数
#include <arpa/inet.h> int inet_aton(const char * string, struct in_addr * addr); // string:含有需转换的IP地址信息的字符串地址值 // addr:将保存转换结果的in_addr结构体变量的地址值 // 成功时返回 1 ,失败时返回 0
示例inet_aton.c
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main() { char * addr = "127.232.124.79"; struct sockaddr_in addr_inet; if (!inet_aton(addr, &addr_inet.sin_addr)) { error_handling("Conversion error"); } else { printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr); } return 0; }
shiqi@inspiron:~/network$ gcc inet_aton.c -o aton shiqi@inspiron:~/network$ ./aton Network ordered integer addr: 0x4f7ce87f
Q:把网络字节序整数型IP地址转换为字符串形式的inet_ntoa函数
#include <arpa/inet.h> char * inet_ntoa(struct in_addr adr); // 成功时返回转换的字符串地址值,失败时返回-1
注意,返回值为char指针。返回字符串地址意味着字符串已经保存在内存空间,但是该函数未向程序员要求分配内存,而是在内部申请了内存保存了字符串
即调用完该函数候要立即把字符串信息复制到其他内存空间。原因是若再次调用inet_ntoa函数可能会覆盖之前保存的字符串信息。即再次调用 inet_ntoa 函数前返回的字符串地址是有效的。长期保存则应复制到其它内存空间
示例inet_ntoa.c
#include <stdio.h> #include <string.h> #include <arpa/inet.h> int main() { struct sockaddr_in addr1, addr2; char * str_ptr; char str_arr[20]; addr1.sin_addr.s_addr = htonl(0x1020304); addr2.sin_addr.s_addr = htonl(0x1010101); str_ptr = inet_ntoa(addr1.sin_addr); strcpy(str_arr, str_ptr); printf("Dotted-Decimal notation1: %s \n", str_ptr); inet_ntoa(addr2.sin_addr); printf("Dotted-Decimal notation2: %s \n", str_ptr); printf("Dotted-Decimal notation3: %s \n", str_arr); return 0; }
shiqi@inspiron:~/network$ gcc inet_ntoa.c -o ntoa shiqi@inspiron:~/network$ ./ntoa Dotted-Decimal notation1: 1.2.3.4 Dotted-Decimal notation2: 1.1.1.1 Dotted-Decimal notation3: 1.2.3.4
Q:套接字创建过程中常见的网络地址信息初始化方法
struct sockaddr_in addr; char * serv_ip = "211.217.168.13"; // 声明IP地址字符串 char * serv_port = "9190"; // 声明端口号字符串 memset(&addr, 0, sizeof(addr)); // 结构体变量 addr 的所有成员初始化为0 addr.sin_family = AF_INET; // 指定地址族 addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的IP地址初始化 addr.sin_port = htons(atoi(serv_port)); // 基于字符串的IP地址端口号初始化
服务器端声明sockaddr_in结构体,将其初始化为赋予服务器端IP和套接字的端口号,然后调用bind函数
客户端声明sockaddr_in结构体,并初始化为要与之连接的服务器端套接字的IP和端口号,然后调用connect函数
Q:服务器端使用INADDR_ANY自动获取运行服务器端的计算机IP地址
struct sockaddr_in addr; char * serv_port = "9190"; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); // 注意这里 addr.sin_port = htons(atoi(serv_port));
若同一计算机已分配多个IP地址(多宿主(Multi-homed)计算机,一般路由器属于这一类),则只要端口号一致,即可从不同IP地址接受数据。服务器优先考虑这种方式
第 4 章 基于 TCP 的服务端/客户端(1)
4.1 理解 TCP 和 UDP
4.2 实现基于 TCP 的服务器/客户端
- Q:基于 TCP 的服务端/客户端函数调用关系
- 1.服务器端创建套接字后连续调用bind、listen函数进入等待状态
- 2.客户端通过调用connect函数发起连接请求。(只能等到服务器端调用listen函数后才能调connect函数,客户端调用connect函数前,服务器端可能率先调用accept函数。当然此时服务器端在调用accept函数时进入阻塞(blocking)状态,知道客户端调connect函数为止)
4.3 实现迭代服务端/客户端:服务端将客户端传输的字符串数据原封不动的传回客户端
Q:迭代服务器端的流程
- 通过插入循环语句反复调用accept函数,可实现继续处理后续客户端连接请求
- 目前该程序同一时刻只能服务于一个客户端
Q:迭代回升服务器端/客户端程序(不完美版本)
服务端echo_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int serv_sock, clnt_sock; char message[BUF_SIZE]; int str_len, i; struct sockaddr_in serv_adr, clnt_adr; socklen_t clnt_adr_sz; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("bind() error"); } if (listen(serv_sock, 5) == -1) { error_handling("listen() error"); } clnt_adr_sz = sizeof(clnt_adr); for (i = 0; i < 5; ++i) { clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); if (clnt_sock == -1) { error_handling("accept() error"); } else { printf("Connected client %d \n", i+1); } // 客户端套接字若调用close函数,这一个循环条件变成假 while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) { write(clnt_sock, message, str_len); } // 针对套接字调用close函数,向连接的相应套接字发送EOF close(clnt_sock); } close(serv_sock); return 0; }
客户端echo_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("connect() error!"); } else { puts("Connceted........"); } while (1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE-1); message[str_len] = '\0'; printf("Message from server: %s", message); } close(sock); return 0; }
编译运行
服务端
shiqi@inspiron:~/network$ gcc echo_server.c -o eserver shiqi@inspiron:~/network$ ./eserver 9190 Connected client 1 Connected client 2
客户端
shiqi@inspiron:~/network$ gcc echo_client.c -o eclient shiqi@inspiron:~/network$ ./eclient 127.0.0.1 9190 Connceted........ Input message(Q to quit): Hi Message from server: Hi Input message(Q to quit): cd Message from server: cd Input message(Q to quit): q
存在问题
在客户端的代码中
write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE-1); message[str_len] = '\0'; printf("Message from server: %s", message);
以上代码有个错误的假设「每次调用 read、write函数时都会以字符串为单位执行实际 I/O 操作」
注意「TCP不存在数据边界」,上述客户端是基于 TCP 的,因此多次调用 write 函数传递的字符串有可能一次性传递到服务端。此时客户端有可能从服务端收到多个字符串。即需要考虑服务端「字符串太长,需要分2个数据包发送」
服务端希望通过调用 1 次 write 函数传输数据,但是如果数据太大,操作系统就有可能把数据分成多个数据包发送到客户端。另外,在此过程中,客户端可能在尚未收到全部数据包时就调用 read 函数
以上的问题都是源自 TCP 的传输特性
第 5 章 基于 TCP 的服务端/客户端(2)
5.1 回声客户端的完美实现
Q:回声客户端的完美实现
因可提前确定接收数据的大小,使用循环控制即可
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char message[BUF_SIZE]; int str_len, recv_len, recv_cnt; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("connect() error!"); } else { puts("Connceted........"); } while (1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } str_len = write(sock, message, strlen(message)); recv_len = 0; while (recv_len < str_len) { recv_cnt = read(sock, message, BUF_SIZE-1); if (recv_cnt == -1) { error_handling("read() error!"); } recv_len += recv_cnt; } message[str_len] = '\0'; printf("Message from server: %s", message); } close(sock); return 0; }
Q:计算器服务端客户端实现
客户端 op_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 #define RLT_SIZE 4 #define OPSZ 4 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char * argv[]) { int sock; char opmsg[BUF_SIZE]; int result, opnd_cnt, i; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage: %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("connect() error!"); } else { puts("Connected...."); } fputs("Operand count: ", stdout); scanf("%d", &opnd_cnt); opmsg[0] = (char)opnd_cnt; for (i = 0; i < opnd_cnt; ++i) { printf("Operand %d: ", i+1); scanf("%d", (int*)&opmsg[i*OPSZ+1]); } fgetc(stdin); fputs("Operator:", stdout); scanf("%c", &opmsg[opnd_cnt*OPSZ+1]); write(sock, opmsg, opnd_cnt*OPSZ+2); read(sock, &result, RLT_SIZE); printf("Operation result: %d\n", result); close(sock); return 0; }
服务端 op_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 #define OPSZ 4 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int calculate(int opnum, int opnds[], char op) { int result = opnds[0], i; switch (op) { case '+': for (i = 1; i < opnum; ++i) { result += opnds[i]; } break; case '-': for (i = 1; i < opnum; ++i) { result -= opnds[i]; } break; case '*': for (i = 1; i < opnum; ++i) { result *= opnds[i]; } break; } return result; } int main(int argc, char * argv[]) { int serv_sock, clnt_sock; char opinfo[BUF_SIZE]; int result, opnd_cnt, i; int recv_cnt, recv_len; struct sockaddr_in serv_adr, clnt_adr; socklen_t clnt_adr_sz; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) { error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("bind() error"); } if (listen(serv_sock, 5) == -1) { error_handling("listen() error"); } clnt_adr_sz = sizeof(clnt_adr); for (i = 0; i < 5; ++i) { opnd_cnt = 0; clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, & clnt_adr_sz); read(clnt_sock, &opnd_cnt, 1); recv_len = 0; while ((opnd_cnt*OPSZ+1) > recv_len) { recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1); recv_len += recv_cnt; } result = calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len-1]); write(clnt_sock, (char *)&result, sizeof(result)); close(clnt_sock); } close(serv_sock); return 0; }
编译运行
47@pc:~/network/ch05$ gcc op_server.c -o serv 47@pc:~/network/ch05$ ./serv 9190 47@pc:~/network/ch05$ gcc op_client.c -o clnt 47@pc:~/network/ch05$ ./clnt 127.0.0.1 9190 Connected.... Operand count: 3 Operand 1: 8 Operand 2: 9 Operand 3: 10 Operator:* Operation result: 720