文章目录
1. 简单的UDP网络程序
我们把udp的协议封装成一个类。
下面我们就开始认识第一个接口:
这个函数的作用是创建套接字。
第一个参数:叫做socket的域,代表你是本地通信还是网络通信。本地通信就填AF_UNIX,网络通信就填AF_INET。
第二个参数:叫做套接字的类型,决定了我们通信时候对应的报文类型。常用的有流式(SOCK_STREAM)和用户数据报(SOCK_DGRAM)。
第三个参数:协议类型,在网络通信中设置成0就行了。
返回值:
此函数执行成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况。既然是文件描述符,OS就会创建struct file结构,用来指向网络通信的属性和方法,然后我们用文件描述符来标识你打开的文件的信息。
这样sockfd就创建成功。
1.1 日志的打印
然后我们再写一个打印日志的头文件:
第一个参数传的是日志等级,第二个参数传的是格式化输出是什么。
必须在[0,3]这个范围才能进来。
可变参数我们使用时,需要定义的东西:
va_start是将离可变参数最近的参数初始化。
va_list其实就是一个char*的指针,va_end是将这个指针设置成NULL。
va_arg的意思是:提参。如果我们是这样传的:
logMessage(DEBUG, “%d”, 10)
那么这里的x就会被提取成10。
我们再看一下关于这些函数。vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度。
第一个参数:用于缓存格式化字符串结果的字符数组。
第二个参数:限定最多打印到缓冲区sbuf的字符的个数为n-1个,因为vsnprintf还要在结果的末尾追加\0。。
第三个参数:格式化限定字符串。
第四个参数:可变长度参数列表。
返回值:成功打印到第一个参数中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。
这样就可以把格式化字符串打印到logInfo数组里。
如果日志等级是FATAL(致命的),那么就设置成stderr,如果不是就设置成stdout。
下面我们就可以进行打印了:
第一个参数是日志等级,第二个参数是时间,第三个参数是哪个用户打印的,第四个参数是消息内容,在这里就是logInfo。
这里可以用环境变量来查看当前用户。
这样我们就可以进行规范的日志打印。
1.2 服务端初始化
创建好套接字后,我们需要绑定网络了。怎么绑定?我们看一下接口:
服务端将把用于通信的地址和端口绑定到socket上。
第一个参数:需要绑定的socket。
第二个参数:是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
因为我们是要网络通信,所以我们要设置sockaddr_in这个结构体。
第三个参数:是第二个参数的大小。
我们创建这个结构体对象时,需要包含头文件:
下面我们使用这个local的时候,我们需要给它初始化:
或者使用memset来初始化。
下面我们需要将local里面的内容设置一下:
sin_family就是你要填的是网络通信还是本地通信。sin_port就是要设置的端口号,因为服务端口号要发送到网络中,所以要进行转换。
有了端口号还需要IP地址:
我们知道:IP地址是字符串风格点分十进制,它是4字节地址,那么我们可以把它转成uint32_t,因为它也是4字节。
我们可以看到sin_addr它的类型是in_addr。
in_addr里面有一个in_addr_t,它的类型是uint32_t。
这里的意思是把s_addr设置成:如果传的IP地址是空,我们就设置成这个宏,因为IP地址需要发到网络,所以需要转化成网络字节序。否则就设置成这个字符串转成的uint32_t。
这个inet_addr函数的意思是:
把这个字符串点分十进制转成in_addr_t类型。
这个宏就是0:
它的意思是:会绑定所有的IP。
inet_addr的意思是:指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们转成网络字节序。
其实这个宏为0,转不转网络字节序其实意义不大。
云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果是虚拟机,随意。
那么这个local是在哪里开辟的空间?
答案是:在用户栈上,它是一个临时变量,需要写入内核中。
1.3 启动服务器并提供服务
我们要进行网络读写,就需要这个函数:
recvfrom函数是用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
第一个参数就是:我们要接受消息的套接字。
第二个参数就是:接受消息要存放的缓冲区。
第三个参数就是:缓冲区长度。
第四个参数就是:调用操作方式,一般设置为0,意思就是阻塞等待。
第五个参数就是:输出型参数,用来指定欲接收数据的网络地址。
第六个参数就是:输入输出型参数,指向第五个参数长度值。
比如,客户端要给我们发送消息,就会保存在第五个参数中。
返回值:成功则返回接收到的字符数,失败返回-1。
这里就开始读取客户端的消息。
读取成功就设置成字符串,读取失败就打印错误日志,然后重新读取。
不仅需要读取对方的消息内容,也要读取对方的IP和端口号,因为服务端也可能给对方发消息。
这里有一个函数inet_ntoa:
这个函数是将4字节的IP转换成字符串风格的IP。
最后进行打印日志。
1.4 客户端
如果一个客户端要连接server必须知道server对应的ip和port。
既然如此,那么我们客户端的运行格式是:./udpClient server_ip server_port。
如果输入的个数不对,就打印使用手册,然后获取我们要访问的服务器的IP和端口号。
在前面的知识,我们学过,两个主机在网络上通信,其实是两个进程在通信,那么客户端也需要创建套接字信息。
那么客户端需不需要bind呢?
肯定是需要的。但是不需要用户自己bind,而是os自动给你bind。
我们知道:所有的客户端软件与服务器通信的时候,客户端必须要有自己的IP和端口号,服务器也必须要有自己的IP和端口号。
那么为什么客户端不需要我们自己绑定呢?
client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了。OS会自己生成端口号帮你去bind,当你不用的时候,会自动帮你销毁。
那么为什么服务端需要我们自己绑定呢?
server提供的服务,必须被所有人知道,并且它不能随意改变。
然后我们填充一下服务器的内容,下一步就可以发送了。
1.5 客户端发送的消息
怎么发送给服务器呢?我们来看一下这个函数:
这个sendto函数第一个参数就是你的套接字,第二个参数就是你要发送的缓冲区里面的消息,第三个参数是你要发送消息的长度,第四个是调用操作方式,一般设置为0,意思就是阻塞等待。第五个参数就是你要给谁发送。
然后我们可以关闭套接字文件描述符。
1.6 服务端的构造函数和析构函数
我们要创建一个UdpServer,我们要传端口号和IP地址。
我们在输入命令时的格式是:./udpServer port [ip],这个端口号是必须带的,但是IP地址可带可不带。不过在云服务器上,IP地址是不能被指定的。
如果输入的参数个数不一样,就打印一下使用手册。
因为得到的是char*的字符串,所以我们需要将它转成整数。
然后创建服务后,初始化,启动就行。
1.7 本地测试
我们在这里先启动服务器,可以看到它的套接字和bind都成功了。现在阻塞等待。
端口号: 从0到1023是公认端口,我们尽量不要使用这些端口。
netstat -nupl这个命令是查看网络UDP类型的端口。可以看到服务已经启动了。
下面我们继续启动客户端:
这个127.0.0.1这个IP地址叫做本地环回,在本主机上通信。它通过网络协议栈时不会发送到网络,而是发送到主机的另外一个进程的缓冲区。
下面我们就可以发消息了:
那么如果客户端给服务器发送消息后,服务器怎么给我们回复呢?
我们把客户端发送的消息中的小写字母转化成大写字母,存到outbuffer缓冲区里,然后sendto给客户端。
我们把消息存到buffer缓冲区里,然后打印出来。
运行结果:
1.8 服务端回复消息给所有客户端
那么客户端有很多,我们该如果把所有的客户端发送的消息都显示出来给每一个客户端呢?
我们这里用一个哈希表来存放在线客户端。前面存放的是一些信息,后面方的是这个客户端的sockaddr_in。
不存在就把这个客户端的信息添加进去。
我们把缓冲区的消息加上这个客户端的IP和端口号,发送到每一个客户端里。
那么客户端又需要做什么呢?
我们把服务器对象给定义到全局。
创建一个线程,主线程发送消息给服务器。
新线程去执行接收消息,并打印。
我们先创建一个命名管道。
每个客户端都可以在这里看到。