计算机网络:Socket网络编程 Udp与Tcp协议 第一弹
目录
1.IP地址和端口号
1.1 如何通信
1.2 端口号详解
1.3 理解套接字socket
2. 网络字节序
3. socket接口
3.1 socket类型设计
3.2 socket函数
3.3 bind函数
4. UDP通信协议
4.1 UDP服务端类
4.2 Udp服务类InitServer函数
4.3 Udp服务类Start函数
4.4 Udp服务主函数
4.5 Udp客户端编写
4.6 运行结果
往期博客粗粒度讲解TCP/IP协议的内容,包含对IP地址讲解:
计算机网络:TCP/IP网络协议-CSDN博客
1.IP地址和端口号
1.1 如何通信
不管是任何主机都是遵守TCP/IP协议,可以大致分为四层,分别是应用层、传输层、网络层和数据链路层。一般用户打开的进程在应用层。
- 当张三使用主机打开许多进程,如抖音进程,浏览器进程和微信进程。其中张三使用微信进程时,会向微信的服务器发送信息。
- 在全网中,IP地址可以用来标识主机,使一台主机具有唯一性。但是,数据不止要千里迢迢传输到微信服务器的主机上,还要将数据传输到服务器主机上特定的进程,由该进程处理数据,再返回给客户端主机。
- 因此,传输数据不是目的,而是手段,处理数据才是目的。端口号就是用来标识一台主机上的某个进程。
IP地址由IPv4和IPv6两个版本,主要讲解IPv4。IPv4版本中,IP地址是由32位二进制组成,占4个字节的整数。通常使用点分十进制表示,如:192.168.2.1。
端口号是一个2字节,16二进制组成的整数。通常一个主机上,一个端口号只能被一个进程占用。
IP地址+端口号就可以表示全网中某一台主机上的一个进程。
1.2 端口号详解
端口号是一个2字节,16二进制组成的整数。通常一个主机上,一个端口号只能被一个进程占用。
- 0 - 1023是知名端口号。HTTP,FTP,SSH,SMTP等应用层协议所占有,且它们的端口号都是固定的。
- 1024 - 65535端口号,可由操作系统动态分配给客户端程序。
当客户端通过网络发送信息给服务端,到达服务端主机的传输层。服务端主机会获取该消息的报头信息,获取端口号交给应用层中的进程。
假设操作系统会创建一个端口号哈希表,哈希表中映射了端口号和进程的pcb。操作系统获取目的端口号,就可以从哈希表中找到目的进程。进程中有文件描述符,以此找到文件缓冲区。操作系统再把收到的数据拷贝到缓冲区中,就可以交由进程处理数据。这是一种方案。
1.3 理解套接字socket
通过前面的讲述,我们知道IP+port可以表示全网中唯一的一个进程。
本质上,网络通信时两个进程代表人进行通信。而源IP地址、源端口号、目的IP地址和目的端口号,就可以标识全网中唯二的进程。
所以,我们将IP地址+port端口号,称之为socket,即套接字。
2. 网络字节序
多字节内存数据存储在内存中,会有存储顺序的问题。按照不同的存储顺序,可以分为大端字节序存储和小端字节序存储,即大小端之分。
- 大端模式,低位字节内容保存在高地址处,高位字节内容保存在低地址处。小端模式则相反。
- 如把0x11223344写入到地址位0x1000处内存中,其中0x44是低位字节数据,0x11是高位字节数据。
源主机通常将发送缓冲区的数据按内存地址低到高的顺序发出。目标主机接收发送的数据,也是按照内存地址低到高的顺序进行接收。
- 如果大小端主机不做统一,可能发送数据是0x11223344,但是接收数据变为0x44332211,造成数据混乱。
- 所以,TCP/IP协议规定,网络数据流应采用大端低字节流,即低地址处存放高位字节内容。
上面的库函数可以做到网络字节序和主机字节序的相互转换。
- 四个库函数中,h表示主机,n表示网络,l表示32位长整数,s表示16位短整数。
- 其中htonl函数,可以将32位长整数从主机字节序转换成网络字节序,通常转换IP地址。而htons,转换的是16的短整数,用于端口号转换。
IP地址中,应用层通常以点分十进制展现,如“192.168.32.1”。但是在网络通信过程中,传输一个点分十进制的IP地址,需要十几个字符,字节数太大,所以IP地址一般会转换成一个32位的整数,大小才4字节。
3. socket接口
3.1 socket类型设计
socket套接字一般划分为三类。
- 网络socket,一般用于网络间通信。
- 本地socket,也称为unix 域间套接字,通常用于本地通信。
- 原始套接字,即raw socket,可以直接作用于网络层,不需要经过传输层的处理。
这么多套接字种类,如果各自做一套接口,必定有大量相似的代码。所以,设计者想设计一种类型,统一所有套接字种类。
如下图所示,sockaddr全称socket address,即套接字地址,是一个套接字结构体类型。
可是在C语言中不能直接支持类型的继承和多态,但是规定不管是种套接字类型,开头的字段是2字节大小的16位地址类型,表示该套接字的种类。这就是做到了类型的继承。
而在第一个地址类型字段后,sockaddr基类有14字节的数据。网络套接字sockaddr_in在16位地址类型后,还有16端口号和32位IP地址。unix域间套接字,作用于本地通信,在16地址类型后,还有108字节的路径名字段。
虽然其他套接字结构体可能与sockaddr结构体内部字段字节数不同,但是可以通过类型强制转换成sockaddr结构体类型,统一用于socket接口函数中。
下面是linux2.4.5内核源代码,sockaddr中第一字段是地址类型,而sa_family_t就是无符号16位短整数。
网络socket结构体是sockaddr_in,第二个字段也是个无符号16位短整数的端口号。第三个字段是IP地址,而in_addr结构体中,有一个无符号32位长整数。
3.2 socket函数
socket函数用于创建套接字,该函数有三个参数。第一个参数指定通信协议族,如果要进行网络通信其中AF_INET表示网络通信。
第二个参数是指定套接字协议,传输层协议有Udp和Tcp协议。
- TCP协议的特点是有连接,具有可靠性,面向字节流,全双工。
- UDP协议的特点是无连接,不可靠,面向数据报。
第三个参数一般前两个参数设置完后,可以直接传0进去。
如果调用socket函数成功,返回值是一个新的文件描述符。如果失败,会返回一个-1。这个文件描述符就可以作为接受和发送信息的缓冲区。
3.3 bind函数
bind函数是一个套接字和网络地址以及端口号绑定起来。
第一个参数是socket函数返回的文件描述符。第二个参数是sockaddr结构体指针,一般使用网络通信,会传sockaddr_in结构体指针,需要进行类型强制转换,第三个参数该结构体的字节数。
4. UDP通信协议
UDP(用户数据报协议)是一种简单的面向数据报的通信协议。下面我们写一个简单的Udp协议的通信服务,客户端向服务器发送消息,服务器接受消息并把接受的消息回显给客户端。
4.1 UDP服务端类
下面是UdpServer.hpp文件的内容,主要把服务端描述成一个UdpServer类。并且初始化一下服务端的端口号,还有其他的字段。
#ifndef __UDP_SERVER__HPP
#define __UPD_SERVER__HPP
#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
const static int gsockfd = -1;
using namespace LogModule;
class UdpServer
{
public:
UdpServer(const uint16_t port = gdefaultport)
:_sockfd(gsockfd)
,_port(port)
,_isrunning(false)
{}
~UdpServer()
{
if (_sockfd > gsockfd)
::close(_sockfd);
}
private:
int _sockfd;
uint16_t _port; //服务器端口号
bool _isrunning; //服务器运行状态
};
#endif
4.2 Udp服务类InitServer函数
下面是UdpServer服务端类中的成员函数InitServer的代码,InitServer函数用于初始化Udp服务。
- 首先使用socket函数创建一个套接字,第一个参数传AF_INET,表示网络通信。第二个参数传SOCK_DGRAM,表示使用Udp协议,第三个参数默认传0即可。
- 接着,填充网络信息。定义一个sockaddr_in结构体,该结构体内部有三个字段需要填充。第一个字段表示什么通信,填AF_INET,表示网络通信。第二字段填端口号,但是得使用htons函数将主机字节序转换成网络字节序,变成大端模式。
- 第三个字段一般来说需要填写机器的IP地址,但是作为服务器,可能客户端发来的消息需要多台服务器处理不同的信息,如果服务器填写固定的IP地址,那么客户端接受服务器的消息后,只能返回给一台服务器。所以服务器的sin_addr中的s_addr一般设置INADDR_ANY。
// ......
const std::string gdefaultip = "127.0.0.1";
const static uint16_t gdefaultport = 8080;
class UdpServer
{
public:
// ......
void InitServer()
{
// 1.创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
std::cout << "socket:" << strerror(errno) << std::endl;
exit(1);
}
std::cout << "socket success, sockfd is: " << _sockfd << std::endl;
// 2、填充网络信息,
// 2.1 没有把socket信息,设置进如内核中,只是填充了结构体!
struct sockaddr_in loacl;
memset(&loacl, 0, sizeof(loacl));
loacl.sin_family = AF_INET;
loacl.sin_port = ::htons(_port); //要被发送给对方的,即要发送到网络中!
loacl.sin_addr.s_addr = INADDR_ANY;
//loacl.sin_addr.s_addr = ::inet_addr(_ip.c_str()); //1. 点分十进制转为整数 2. 转成网络字节序
// 2.2 bind 设置如内核中
int n = ::bind(_sockfd, (struct sockaddr*)&loacl, sizeof(loacl));
if(n < 0)
{
std::cout << "bind: "<< errno << " " <<strerror(errno) << std::endl;
exit(2);
}
std::cout << "bind success" << std::endl;
}
// ......
private:
int _sockfd;
uint16_t _port; //服务器端口号
bool _isrunning; //服务器运行状态
};
4.3 Udp服务类Start函数
下面是Udp的Start成员函数的代码,用于接收客户端的信息,并回显给客户端。
首先将服务状态设置为真。再写个死循环,一般服务端的程序是不轻易下线的。只有while结束,服务状态再设置成假。
定义一个sockaddr_in结构体变量,用于接收客户端的IP地址和端口。读取网络传过来的信息,可以使用recvfrom函数。下面的recvfrom函数原型,跟read函数类似。
- 第一个参数填socket函数创建的文件描述符。第二个参数填一个指针类型的变量,用于接受传过来的信息,我们默认传过来的是字符串。第三个参数是该指针变量指向空间的大小。第四个参数是个标记位,填写0即可。
- 第五个参数是sockaddr结构体指针类型,需要填入sockaddr_in变量,并强转成sockaddr类指针。第六个参数是类型socklen_t,socklen_t就是一个无符号的32位整数类型,里面要记录sockaddr_in类型的大小。
- 如果该函数调用成功,返回值是一个大于0的整数,表示接受信息的字节数大小。如果失败返回-1。如果返回0,表示读取到文件末尾,或者客户端已关闭
接着n大于0,表示接受到有效信息。需要在下标为n的位置加上反斜杠0。调用ntohs函数可以将网络字节序转换成主机字节序,获取端口号。且inet_ntoa函数可以将sockaddr_in中无符号整数IP地址,转换成点分十进制的字符串。
最后拼接一下字符串,使用sendto函数,转发信息。sendto函数的前四个参数跟recvfrom函数一样。
- 第五个参数需要传客户端的sockaddr_in类结构体变量,里面包含客户端的IP地址和端口号。
- 最后一个参数也是socklen_t类型参数,里面填上dest_addr变量实际的大小即可。
class UdpServer
{
public:
// ......
void Start()
{
_isrunning = true;
while(true)
{
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必须设定
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)(&peer), &len);
if (n > 0)
{
// 在后面字符串后面加上反斜杠0,以便于输出
inbuffer[n] = 0;
// 1.消息内容 && 2.谁发给我的
uint16_t clientport = ::ntohs(peer.sin_port);
std::string clientip = ::inet_ntoa(peer.sin_addr);
std::string clientinfo = clientip + ":" + std::to_string(clientport) + "# " + inbuffer;
std::cout << clientinfo << std::endl;
std::string echo_str = "echo# ";
echo_str += inbuffer;
::sendto(_sockfd, echo_str.c_str(), echo_str.size(), 0, (struct sockaddr*)(&peer), sizeof(peer));
}
}
_isrunning = false;
}
// ......
private:
int _sockfd;
uint16_t _port; //服务器端口号
bool _isrunning; //服务器运行状态
};
4.4 Udp服务主函数
Udp服务端只需要绑定一个端口号,所以在传命令号时,强制传两个参数。其中需要把命令行第二参数转换成整数。
然后再使用智能指针初始化服务类变量,运行InitServer函数和Start函数即可。
#include "UdpServer.hpp"
// ./server_udp localport
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
return 3;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
4.5 Udp客户端编写
客户端主代码编写时,可以在调用该程序的命令行参数后,加上访问的IP地址和端口号。其中要把端口号转换成整数类型。
实现网络通信,第一个也是创建socket套接字。然后,再填充服务端的网络信息。
但是不需要填写自己的网络信息,并且不用bind函数进行绑定。在客户端第一次发消息时,操作系统会动态绑定端口号,而IP地址是传输层的下一层网络层。
再下来就是使用sendto函数发消息,recvfrom函数接受消息。
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
Die(ExitError::USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1.创建socket
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
Die(ExitError::SOCKET_ERR);
}
// 1.1 填充server信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = ::inet_addr(serverip.c_str());
// 2. clientdone
while(true)
{
std::cout << "Please Enter# ";
std::string message;
std::getline(std::cin, message);
// client必须要也要有自己的ip和端口号!但是客户端,不需要自己显示的调用bind!!
// 而是,客户端首次sendto消息的时候,由OS自动进行bind
int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
(void)n;
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
n = ::recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&temp), &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
4.6 运行结果
创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!