文章目录
- 一、IP地址和端口号
- 二、网络字节序
- 三、socket编程接口
- 1.socket常见API
- 2.sockaddr结构
- 四、UDP套接字
- 1.简单认识UDP协议
- 2.利用UDP套接字实现网络版本大小写转换
- (1)服务端
- (2)客户端
一、IP地址和端口号
IP协议目前有两个版本,分别是IPV4和IPV6,IP地址是在IP协议中用来标识网络中不同主机的地址。在TCP/IP五层模型中网络IP层的IP数据包中,有两个IP地址,一个是源IP地址,一个是目的IP地址。源IP地址指的是发送信息一方的IP地址,目的IP地址指的是接收信息一方的IP地址。
然而,我们使用网络进行数据交互的时候,并不只是两台主机在信息交互,本质上是两台主机上的进程在进行信息交互。所以网络传输除了需要IP地址,还需要端口号。
端口号是传输层协议的内容,它用来标识一台主机上的一个进程的唯一性。端口号是2字节的16位整数,一个端口号只能被一个进程占用,端口号存在的意义是当对方主机将数据发送过来的时候,操作系统拿到这个数据之后通过端口号来确定要将数据交给哪一个进程来处理。传输层协议(TCP和UDP)的数据段中也有两个端口号,一个是源端口号,一个是目的端口号,它们分别用来描述数据是谁发的以及数据要发给谁。
IP地址标定互联网中一台主机的唯一性,端口号标定一台主机中进程的唯一性,所以IP地址和端口号共同标定互联网中进程的唯一性。网络通信的本质也是进程间通信。
二、网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,有些机器是大端序列(即低地址高字节),而有些机器是小端序列(即高地址低字节),由于不同机器的字节序不同,所以就会存在大端序列机器通过网络给小端序列机器发消息时出现数据错乱的问题。为了解决这个问题,TCP/IP协议规定,网络数据流应采用大端字节序。
不管你的主机是大端序列机器还是小端序列机器,都要按照这个TCP/IP协议规定的网络字节序来发送或者接收数据。
如果你的主机是小端序列的机器,就需要先将数据转换成大端序列再进行网络发送,大端序列的机器可以忽略转换直接发送。
在接收数据的时候,如果你的主机是小端序列的机器,就需要先将网络接收下来的数据转成小端序列,这样你的机器才能正确读取数据。如果是大端序列的机器同样可以忽略转换直接读取。
C语言的库函数为我们提供了网络字节序和主机字节序转换功能,一共有四个函数,只需要调用这些函数就可以实现网络字节序和主机字节序之间的相互转换:
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位短整数。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
三、socket编程接口
1.socket常见API
socket就是套接字的意思,socket的API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6。socket网络编程接口分为两类,一类是用来支持网络通信的,这种叫作网络套接字;还有一类是用来支持本地通信的,这种叫做域间套接字,也被称作双向管道。
下面是几个常见的socket套接字编程接口:
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器):
形参列表中domain表示的是socket的域,它用来标识是要进行网络通信还是本地通信;type表示的是套接字的类型,它用来标识我们通信的时候对应的报文类型,报文类型分为流式类型和用户数据报类型;protocol表示的是协议类型,网络通信时该参数设置为0即可。socket函数的返回值是int类型,如果创建socket成功则返回文件描述符 否则返回-1。
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* address_len);
建立连接 (TCP, 客户端):
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2.sockaddr结构
由于网络套接字需要一套接口多种用途,既要支持网络通信,也要支持本地通信,但是网络通信的接口和本地通信的接口所需要的参数数据肯定是不一样的,所以socket这一套接口的设计者就设计了一套抽象结构,也就是sockaddr结构:
他们一共设计了三个结构体,分别是sockaddr
、sockaddr_in
和sockaddr_un
,其中sockaddr_in
对应的是网络通信的结构,sockaddr_un
对应的是本地通信的结构,它们的前16位用来标定地址类型,网络通信结构的地址类型是AF_INET,本地通信结构的地址类型是AF_UNIX。
sockaddr
结构是设计出来的第三个结构,它用来统一sockaddr_in
结构和sockaddr_un
结构,在使用的时候我们先定义sockaddr_in
结构或者sockaddr_un
结构,然后强转成sockaddr
结构,它会通过前16位的地址类型来确定这是要进行网络通信还是本地通信。
网络通信必须有的两个数据分别是端口号和IP地址。在sockaddr_in
中描述端口号的字段是sin_port,描述IP地址的字段是sin_addr。
端口号由程序员指定提供,但是需要注意的是端口号一定是网络通信双方都要拿到的,所以端口号一定会通过网络发送给对方,那么我们在使用的时候就要注意将该字段转换成网络字节序。另外,在指定端口号时不要指定0-1023之间的端口号,因为系统中一些网络服务都有自己的确定端口,重复的端口号必然导致服务无法启动,会造成冲突。
IP地址在sockaddr_in
的sin_addr字段中,sin_addr其实也是一个结构体,它里面只封装了一个in_addr_t类型的变量,一般在设置IP地址的时候,我们将其设置成INADDR_ANY,这其实是一个宏,就是一个全0的数字,但这个宏有特殊的含义,它代表我们不关心会连接到哪一个IP地址,直接让操作系统帮我们连接到任意的IP地址,一般在写服务器的时候非常推荐将IP地址字段设置成INADDR_ANY。
云服务器禁止我们连接云服务器上的所有确定的IP地址,所以云服务器上只能使用INADDR_ANY填充IP字段。
四、UDP套接字
1.简单认识UDP协议
UDP协议的全称是User Datagram Protocol,即用户数据报协议,它是传输层协议,它的特点是无连接的,不可靠传输,以及面向数据报的传输,在socket函数的形参type中,SOCK_DGRAM代表的就是UDP套接字。
2.利用UDP套接字实现网络版本大小写转换
(1)服务端
UDP套接字服务端首先要做的是解决网络的问题,然后再来扩展业务。UDP套接字建立网络通信的步骤比较简单,首先是创建套接字,然后填充端口号、IP地址等网络信息,最后bind网络信息即可完成网络通信渠道的建立。
所以我们先将UdpServer的网络通信搭建起来,首先是构造函数我们需要传递进端口号和IP地址来初始化服务器的端口号与IP地址,由于云服务器是禁止我们bind任何IP地址的,所以我们给IP地址默认为空串,使用INADDR_ANY来填充IP地址字段。
然后是创建套接字和bind网络信息,在bind网络信息之前先要填充sockaddr结构,由于我们实现的是网络通信,所以应该使用sockaddr_in,填充sockaddr_in里的端口号、协议家族和IP地址之后,再将sockaddr_in类型强转成sockaddr类型,因为bind函数接口的参数只支持sockaddr类型。
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
static void Usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}
class UdpServer
{
public:
UdpServer(int port, const string& ip = "")
: _sockFd(-1), _port(port), _ip(ip)
{
}
~UdpServer()
{}
// 服务器初始化
void init()
{
// 1.创建套接字
_sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockFd < 0)
{
cerr << "socket error" << endl;
exit(1);
}
cout << "socket success" << endl;
// 2.bind
// 2.1填充网络信息
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = (_ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()));
// 2.2bind网络信息
if(bind(_sockFd, (const sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
exit(2);
}
cout << "bind success" << endl;
}
// 启动服务器
void start()
{
while(true)
{
cout << "server running" << endl;
sleep(1);
}
}
private:
int _sockFd; // sock文件描述符
uint16_t _port; // 端口号
string _ip; // IP地址
};
// ./udpServer port [ip]
int main(int argc, char* argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if (argc == 3)
{
ip = argv[2];
}
UdpServer svr(port, ip);
svr.init();
svr.start();
return 0;
}
搭建好网络通信的渠道以后,我们就可以让服务器运行起来,为客户端提供服务了:
我们要实现一个大小写转换功能的程序,客户端通过网络发送信息给服务端,服务端接收到信息之后对其进行处理,处理完成之后将转换好的信息再返回给服务端,主要是演示UDP套接字实现网络通信的过程。
所以我们在服务器启动之后,首先将服务器设置成死循环(因为服务器不能随便地挂机),将所有的服务写在这个死循环内。然后是从客户端读取信息,将信息的小写字母全部转换成大写字母,然后再将信息发送回给客户端。
UDP套接字的网络通信采用的读取接口是recvfrom函数,发送接口是sendto函数:
recvfrom:
- int sockfd:传入创建好的socket文件描述符即可。
- void * buf:需要将读取上来的数据保存到这个buf中。
- size_t len:填写一次需要读取的大小。
- int flags:设置为0,阻塞式读取数据。
- struct sockaddr * src_addr:这是一个输入输出型参数,用来接收远端的sockaddr结构信息。
- socklen_t * addrlen:这也是一个输入输出型参数,用来接收远端sockaddr结构体的大小。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sendto:
- int sockfd:传入创建好的socket文件描述符即可。
- const void * buf:将buf中的内容发送到对端。
- size_t len:填写一次需要发送的大小。
- int flags:设置为0,阻塞式发送数据。
- struct sockaddr * src_addr:这是一个输入型参数,用来填写远端的sockaddr结构信息。
- socklen_t * addrlen:这也是一个输入型参数,用来填写远端sockaddr结构体的大小。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
服务端总代码:
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
static void Usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}
class UdpServer
{
public:
UdpServer(int port, const string &ip = "")
: _sockFd(-1), _port(port), _ip(ip)
{
}
~UdpServer()
{
}
// 服务器初始化
void init()
{
// 1.创建套接字
_sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockFd < 0)
{
cerr << "socket error" << endl;
exit(1);
}
cout << "socket success" << endl;
// 2.bind
// 2.1填充网络信息
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = (_ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()));
// 2.2bind网络信息
if (bind(_sockFd, (const sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
exit(2);
}
cout << "bind success" << endl;
}
// 启动服务器
void start()
{
char inbuffer[1024];
char outbuffer[1024];
while (true)
{
sleep(1);
sockaddr_in peer; // 用来接收远端的网络信息
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockFd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr *)&peer, &len);
if (s == -1)
{
cerr << "recvfrom error" << endl;
continue;
}
if (s > 0)
{
inbuffer[s] = '\0';
}
// 大小写转换
for (int i = 0; i < strlen(inbuffer); i++)
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
{
outbuffer[i] = toupper(inbuffer[i]);
}
else
{
outbuffer[i] = inbuffer[i];
}
}
// 将转换后的数据发送回给客户端
sendto(_sockFd, outbuffer, strlen(outbuffer), 0, (const sockaddr *)&peer, len);
}
}
private:
int _sockFd; // sock文件描述符
uint16_t _port; // 端口号
string _ip; // IP地址
};
// ./udpServer port [ip]
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if (argc == 3)
{
ip = argv[2];
}
UdpServer svr(port, ip);
svr.init();
svr.start();
return 0;
}
(2)客户端
UDP套接字客户端首先也是要解决网络问题才能进行通信,所以首先要创建套接字。然后我们需要知道服务器的IP地址和端口号,有了这两个东西我们才能知道要跟谁进行网络通信。
客户端与服务端不同的是,创建了套接字之后,客户端不需要bind网络信息,准确来说应该是不需要自己手动bind网络信息,让操作系统帮我们自动bind网络信息。非常不推荐自己手动bind网络信息,原因是如果我们的客户端在代码实现的地方就手动指定端口号,而其它客户端都采用操作系统指定的端口号,就有可能出现其它客户端先启动,并且操作系统给它指定的端口号跟我们客户端的端口号相同,那么我们的客户端就启动不了了。所以只有服务端要手动bind端口号,客户端只需要让操作系统帮我们自动bind端口号即可。
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
static void Usage(std::string name)
{
std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 根据命令行,设置要访问的服务器IP
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2.创建套接字
int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockFd < 0)
{
cerr << "socket error" << endl;
}
// 3.通信
// 3.1填写远端信息
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
string outbuffer;
char inbuffer[1024];
while(true)
{
cout << "Please Enter #";
getline(cin, outbuffer);
sendto(sockFd, outbuffer.c_str(), outbuffer.size(), 0, (const sockaddr*)&server, sizeof(server));
sockaddr_in tmp;
memset(&tmp, 0, sizeof(tmp));
socklen_t len = sizeof(tmp);
ssize_t s = recvfrom(sockFd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&tmp, &len);
if(s > 0)
{
inbuffer[s] = '\0';
cout << "server echo# " << inbuffer << endl;
}
}
return 0;
}