Ubuntu16.04 下 C/C++,通过socket编程实现server与client间通信时遇到的问题。
类型转换
x86CPU都是小端字节序(little-endian),地址低位存储低位值。而网络字节序与CPU类型和操作系统无关,是大端字节序(big-endian),低位地址存储高位值,从地址小向大(左向右)看符合人的直观感受。
htonl()
和htons()
等函数将主机字节序转换为网络字节序。
1 2 3 #include <arpa/inet.h> servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT);
通常需要将命令行中输入的点分十进制ip地址转换为二进制类型的地址,或者将二进制类型的地址转换为点分十进制ip地址并输出显示。
函数原型
1 2 int inet_pton (int af, const char * src, void * dst) ;int inet_ntop (int af, const void * src, char * dst, socklen_t cnt) ;
将命令行输入的地址存入字符串,使用inet_pton()
处理。
1 2 3 #include <sys/socket.h> #include <arpa/inet.h> inet_pton(AF_INET, argv[1 ], &servaddr.sin_addr);
bzero()和memset() 有时需要执行清0操作。
1 2 #include <string.h> void bzero (void *s, int n)
将s所指内存的前n个字节清0
bzero()
只有2个参数,它不是ANSI C函数,其起源于早期的Berkeley网络编程代码,但是几乎所有支持套接字API的厂商都提供该函数。
1 2 #include <string.h> void *memset (void *buffer, int c, int count)
将buffer所指内存的前count个字节设为c
memset()
除清0外还能设其他值,但在第2和3个参数互换位置时,编译器不能发现。它是ANSI C函数,更常规、用途更广。
如果只想清0,则用bzero()
更方便。在设置Internet socket地址前,先将其所有字节清零。因为struct sockaddr_in
中有8个填充字节,清0避免异常。
1 2 struct sockaddr_in servaddr ;bzero(&servaddr, sizeof (servaddr));
使用INADDR_ANY作为ip地址 在为udp服务器程序设置ip地址时,使用INADDR_ANY做ip。
1 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
winsock中定义
1 #define INADDR_ANY (ULONG)0x00000000
INADDR_ANY即表示0.0.0.0,也就是“任意地址”、“所有地址”,在这里表示本机的所有IP。
有些主机的网卡数量不止一块,如果某套接字只绑定某一特定的ip,那么它就只能监听此ip地址所对应的网卡端口。如果想同时监听多个物理端口,则需要写3个套接字,较为繁琐。
而绑定0.0.0.0时,只需要管理1个套接字,此时无论数据从哪一网卡端口进入,都可以收到。因此使用INADDR_ANY作为ip,减少管理开销。
初步使用线程 无论是server还是client,收信和发信都要同步进行,因而需要使用至少2个线程。
以client为例,创建一个收信线程和一个发信线程。截取部分代码如下:
1 2 3 4 5 6 #include <thread> #include <functional> std ::thread t1 (std ::bind(&ClientSession::readThread, this )) ;t1.detach(); std ::thread t2 (std ::bind(&ClientSession::writeThread, this )) ;t2.join();
t2.join();
的目的是让当前主线程等待所有的子线程执行完,才执行主线程能后续语句
t1.detach();
的目的是让子线程脱离主线程,二者独立运行。
以上代码在创建线程时调用了类内的函数,因而比较繁琐。一般而言,创建线程只需
学画流程图 撰写实验报告时需要画流程图,用markdown画
1 2 3 4 5 6 7 8 9 10 st=>start: 开始 init=>operation: 初始化 login=>operation: 登录、选择联系人、开启读写会话子线程 wait=>condition: 读写子线程是否结束 ed=>end: 结束 ed2=>end ed3=>end st->init->login->wait wait(yes)->ed wait(no)->wait
1 2 3 4 5 6 7 8 9 10 st=>start: 开始 init=>operation: 初始化 login=>operation: 登录、选择联系人、开启读写会话子线程 wait=>condition: 读写子线程是否结束 ed=>end: 结束 ed2=>end ed3=>end st->init->login->wait wait(yes)->ed wait(no)->wait
1 2 3 4 5 6 st=>start: 收数据线程开始 t=>condition: 是否收到字符串"bye" ed=>end: 收数据线程结束 st->t t(yes)->ed t(no)->t
1 2 3 4 5 6 st=>start: 收数据线程开始 t=>condition: 是否收到字符串"bye" ed=>end: 收数据线程结束 st->t t(yes)->ed t(no)->t
1 2 3 4 5 6 st=>start: 读数据线程开始 t=>condition: 是否发送字符串"exit" ed=>end: 读数据线程结束 st->t t(yes)->ed t(no)->t
1 2 3 4 5 6 st=>start: 读数据线程开始 t=>condition: 是否发送字符串"exit" ed=>end: 读数据线程结束 st->t t(yes)->ed t(no)->t
1 2 3 4 5 6 7 8 9 st=>start: 开始 init=>operation: 初始化 login=>operation: (若存在,则)处理用户的登陆、指定 联系人、开启关闭会话和信息传送等请求 exit=>condition: 是否关闭服务器进程 ed=>end: 结束 st->init->login->exit exit(no)->login exit(yes)->ed
1 2 3 4 5 6 7 8 9 st=>start: 开始 init=>operation: 初始化 login=>operation: (若存在,则)处理用户的登陆、指定 联系人、开启关闭会话和信息传送等请求 exit=>condition: 是否关闭服务器进程 ed=>end: 结束 st->init->login->exit exit(no)->login exit(yes)->ed
学画时序图 撰写实验报告时需要画时序图,用markdown画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Title: 收发数据时序图 Note left of PC1: PC1客户端进程 PC1->server: hi! here is PC1. server->PC2: hi! here is PC1. Note right of PC2: PC2客户端进程 PC1->server: I'm PC1. server->PC2: I'm PC1. PC2-->server: hello, I'm PC2. server-->PC1: hello, I'm PC2. PC1->server: exit note over PC1,server: 会话结束 server->PC2: exit PC2-->server: exit note over server,PC2: 会话结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Title: 收发数据时序图 Note left of PC1: PC1客户端进程 PC1->server: hi! here is PC1. server->PC2: hi! here is PC1. Note right of PC2: PC2客户端进程 PC1->server: I'm PC1. server->PC2: I'm PC1. PC2-->server: hello, I'm PC2. server-->PC1: hello, I'm PC2. PC1->server: exit note over PC1,server: 会话结束 server->PC2: exit PC2-->server: exit note over server,PC2: 会话结束