网络编程
- 1. 网络基础编程知识
- 1.1网络字节序问题
- 1.2 常用socket编程接口
- 1.2.1 sockaddr
- 1.2.2 ip地址转换函数
- 1.2.4 socket()
- 1.2.3 bind()
- 1.2.4 listen()
- 1.2.5 accept()
- 1.2.6 connect()
- 1.3 以udp为基础的客户端连接服务器的demo
- 1.4 以udp为基础的的服务器聊天室功能demo
- 1.5 基于TCP连接的具有线程池功能的服务器客户端demo
- tcp_test目录
- sing_fock_test目录
- thread_tcp目录
- tcpthreadpool目录
- 网络的理论部分
1. 网络基础编程知识
1.1网络字节序问题
已知计算机的数据存储有大小端之分,网络流数据同样有大小端之分。
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的数据顺序发出。
- 接收主机把从网络上接到的字节依次保存在缓冲区中,也是按地址从低到搞的顺序保存。
- 因此网络数据流规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定:网络流数据应采取大端字节序,即低地址高字节(符合人类的阅读习惯)。
- 如果当前发送主机是小端,那么需要改成大端再发送!
为了解决这个问题,使网络具有可移植性,使同样的代码在大小端机器上都能运行,需要使用下面的库函数做网络字节序和主机字节序的转换。
# include<arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
解释:
- 其中h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- htonl 表示将32位的长整形数从主机字节序转化为网络字节序。例如:将IP地址转化后发送。
- 若主机是小端字节序,这些函数将做大小端转换再返回;否则原封不动返回。
从参数和返回值可以看出,这个函数是转换整型的函数,比如说port接口转换就会用到该函数
如图:atoi函数把string转化成整形,然后交给htons转化成网络字节流的数据格式!
1.2 常用socket编程接口
socket API是一层抽象的网络编程接口,适用于各种底层网络协议。如IPv4、v6等。
//网络编程常用的四个接口
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <arpa/inet.h>// 创建 socket 文件描述符(TCP/UDP, 客户端+服务器)
int socket(int domain, int type, int protocol);//绑定端口号(TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP,服务器)
int listen(int socket, int backlog);//接受请求(TCP,服务器)
int accept(int socket, struct sockaddr* address, socklen_t* addrlen);//建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
1.2.1 sockaddr
sockaddr可以认为是存放,将要访问的服务器的ip地址和端口号的结构体。
因为各种网络协议地址格式并不同,所以为了适配格式,产生了sockaddr(通用的地址结构)。
以bind为例(accept、connect都一样),AF_INET就指定了将要通信的地址类型,所以再传入sockaddr之后,程序会根据socket类型自动转化!
这个相当于c语言的多态。(调用同一个函数,会有不同的效果!)
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号地址和32位IP地址。
- IPv4和IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要具体知道是哪种类型的sockaddr结构体,就可以根据16位类型字段确定结构体中的内容。
- socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in;好处就是增加了程序的通用性。
下面是v4和v6的sockaddr地址结构:
1.2.2 ip地址转换函数
功能:实现点分十进制字符串和无符号32位整数之间相互转化!
inet_addr()函数功能介绍:
在1.2.1这一节我们发现,ipv4其实是无符号整数,然而我们在访问ip地址时,使用的是点分十进制的方式。这就要求我们把点分十进制的字符串转化成无符号整数。
比如:ip = “192.168.1.1” ->xxxxxxxx …xxxxxxxx 这种形式, 我们可以使用atoi这种函数一个一个转化。
但是可以使用inet_addr()接口,可以将点分十进制直接转化。
如下图所示:
inet_aton()函数介绍:将ascii码形式的点分十进制转化成网络需要的无符号数。
void func(){char* _ip="192.168.1.1";struct sockaddr_in local;inet_aton(_ip, &(local.sin_addr));std::cout<<"aton转化前_ip: "<<_ip<<std::endl;std::cout<<"aton转化后无符号整数"<<local.sin_addr.s_addr<<std::endl;}//aton转化前_ip: 192.168.1.1//aton转化后无符号整数16885952
inet_ntoa()函数介绍:将网络的无符号数转化成点分十进制:
int main()
{// initServer();struct sockaddr_in local1;struct sockaddr_in local2;local1.sin_addr.s_addr = 0;local2.sin_addr.s_addr = 0xffffffff;char *result1 = inet_ntoa(local1.sin_addr);char *result2 = inet_ntoa(local2.sin_addr);std::cout << "第一次调用ntoa:restult1: " << result1 << std::endl; std::cout << "第二次调用ntoa:restult2: " << result2 << std::endl;return 0;//第一次调用ntoa:restult1: 255.255.255.255//第二次调用ntoa:restult2: 255.255.255.255
}
- 为什么result1和result2的结果一样?
因为手册上说了,ntoa函数是系统申请了一个静态地址空间,存放了返回值。当再次调用时,静态地址被覆盖了,因此就被改变了。这个例子变相的说明了它可能不是一个线程安全的函数!!!
验证一下是不是线程安全的?
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);sleep(1);printf("addr1: %s\n", ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);sleep(1);printf("addr2:%s\n", ptr);}return NULL;
}
int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
根据结果可知,在centos7上,该函数是线程安全的,内部应该加了锁。
建议:
- 在多线程下,推荐使用inet_ntop函数,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
1.2.4 socket()
将本机的网络号和端口号
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样,返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write 在网络上收发数据;
- 如果调用出错,socket返回-1;
- 对于IPv4,domain参数为AF_INET;IPv6为AF_INET6。
- 对于TCP协议,type参数可以指定为SOCK_STREAM,表示面向流的传输协议;对于UDP协议,指定为SOCK_DGRAM。
- 第三个参数默认为0即可。
1.2.3 bind()
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
- sockaddr* 存的是一个通用指针类型,存的是本地的IP和port,第三个参数是结构体的长度;
初始化sockaddr可以这样初始化:
struct sockaddr_in local; bzero(&local, sizeof local); //初始化为0,类似于memsetlocal.sin_family = AF_INET; //指明famaily为ipv4地址协议//服务器的IP和端口未来也是要发送给对方主机的 ->先要将数据发送到网络!local.sin_port = htons(_port); //将host的整形,转化为net的string类型//1.同上,将点分十进制字符串风格IP地址->4字节//2. 然后4字节主机序列->网络序列// 我们可以创建子进程帮我们完成这个工作,但是我们有一套接口,可以帮助我们完成这个工作local.sin_addr.s_addr = _ip.empty()?INADDR_ANY:inet_addr(_ip.c_str()); //如果我们没自己写ip地址,服务器会自动给分配一个!,这样,只要端口号正确,服务器就能收到消息!
- INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;
1.2.4 listen()
- listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置一般不会太大(一般是5)
- listen() 成功返回0,失败返回-1;
1.2.5 accept()
- 三次握手完成后,服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个输出型参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr传NULL,表示不关心客户端的地址。
- addrlen参数时一个传入传出参数,传入的是调用者提供的缓冲区addr的长度,传出的时客户端地址结构体的实际长度。
简单理解一下流程:
客户端输入listen激活的sock的IP和port,然后服务器accept后,再产生一个sockfd来为客户端服务。
相当于门口有人把你领进来了之后,又分配了一个服务员来服务你,以后有什么事就直接叫服务员就好了!
1.2.6 connect()
- 客户端需要调用connect()连接服务器;
- connect和bind的参数一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1;
1.3 以udp为基础的客户端连接服务器的demo
功能1:客户端输入消息,服务器收到消息,并返回给客户端。
功能2:客户端输入linux指令,服务器收到指令,并返回给客户端结果消息。
- 功能1使用:udp_server copy.hpp需要编译这个文件
./server 8080 运行服务器的可执行程序
./client 123.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息即可!
- 功能2使用:udp_server .hpp需要编译这个文件
./server 8080 运行服务器的可执行程序
./client 127.0.0.1 8080 连接服务器ip地址,和端口号, 输入linux命令即可
源码地址
1.4 以udp为基础的的服务器聊天室功能demo
功能1:chat_no_thread文件夹,实现的是可以多个客户端连接服务器,但是都是各发各的消息,客户端不互通。
功能2: chat_thread_success 文件夹,实现的是可以多个客户端连接服务器,客户端消息互通,相当于群聊功能。
./server 8080 运行服务器的可执行程序
./client 123.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息即可!
- 功能2使用:udp_server .hpp需要编译这个文件
./server 8080 运行服务器的可执行程序
./client 127.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息即可!
源码地址
1.5 基于TCP连接的具有线程池功能的服务器客户端demo
目录结构:
tcp_test目录
该目录下实现了基本的TCP连接的服务器功能,但是客户端没有写。
可以编译运行服务器成功后,使用telnet命令进行测试!
功能:服务器接受消息,并且返回给客户端。
./server 8080 运行服务器的可执行程序
telnet 127.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息测试即可!
sing_fock_test目录
服务器端版本有三种:
/*
主要包含两个版本
版本1:单进程版,会阻塞
版本2:多进程版,会阻塞
版本2.1:多进程版本,变成个孤儿进程,不会阻塞!
*/
其中版本1:是单进程版,意思就是说服务器一次只能建立一个链接,断开后才能建立第二个链接。
版本2是多进程版,虽然子进程直接退出了,但是父进程得阻塞等待,所以说服务器也会阻塞等待它。
版本2.1:子进程再fork后,子进程立马退出(父进程就不会阻塞了),就会编程孤儿进程,孤儿进程被OS领养,因此就不会阻塞父进程。
使用:
./server 8080 运行服务器的可执行程序
./client 123.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息即可!
源码地址
thread_tcp目录
线程版本,服务器端通过线程来为客户端提供服务建立TCP链接,这样不会阻塞主进程,主进程只管监听,线程管进行和客户端通信。
使用:
./server 8080 运行服务器的可执行程序
./client 123.0.0.1 8080 连接服务器ip地址,和端口号, 然后输入消息即可!
源码地址
tcpthreadpool目录
该服务是线程池版本的,预先申请好线程,然后等待使用。这样可以降低频繁申请的时间。
客户端版本有三种:
/*
主要包含三个版本
版本1(tcp_client copy 2.cc):发消息就建立连接,发完自动断开,客户主动断开,服务器不会断开!
版本2(tcp_client copy.cc):常链接,一个线程为一个人服务,不会自动断开。
版本3(tcp_client.cc):发消息就建立连接,发完自动断开,change和英汉互译服务,客户主动断开,服务器也会主动断开!
*/
服务器有三个功能:小写转大写,英汉互译功能,都在server函数里面。
源码地址
网络的理论部分
理论部分介绍