本文主要介绍网络编程的相关知识,在正式介绍网络编程之前,我们得先了解一些前置的知识。
1、端口号
我们上网其实就是两种动作,一个是将远处的数据拉取到本地,另一个是把我们的数据发送给远端。其实大部分的网络通信行为都是用户触发的,而在计算机中,谁表示用户呢?答案是进程。当用户接受到数据后,OS要将对应的数据给特定的服务进程。一台机器上有许多的服务进程,而我们用**特定的端口号(port)**来标识这个服务进程。
所以当我们将IP与端口号进行绑定,就可以得出互联网中的唯一一个进程了。这种方式也叫做套接字通信,服务器端和客户端的通信一般都采用这种方式。
为什么我们要用端口号来标识一个进程呢?不是有进程的pid吗?首先是因为进程的pid是不断变化的,其次如果使用pid的话,网络服务就和操作系统进行强关联了。此时如果我们修改一下操作系统对进程的标识方式,那么对上层的网络服务也需要进行大幅修改。另外OS中并不是每个进程都有端口号的,一般只有网络进程才有端口号。端口号和进程pid在OS会通过特定数据结构关联起来,例如哈希表。
2、TCP和UDP协议
在正式开始介绍网络编程之前,我们先简单了解一下UDP和TCP协议(后面具体介绍)
TCP(传输控制协议)和UDP(用户数据报协议)是互联网上用于数据传输的两种重要协议,它们的主要区别在于以下几个方面:
-
连接性:
- TCP 是面向连接的协议,意味着在数据传输之前,需要建立一个连接,在传输完成后需要断开连接。
- UDP 是无连接的,它发送数据之前不需要建立连接,数据可以直接发送给接收方。
-
可靠性:
- TCP 提供了可靠的服务。它确保数据包的顺序传输,并且通过确认和重传机制保证数据的完整性。
- UDP 不保证数据的可靠传输,它只负责发送数据,不保证数据包的顺序或是否到达。
-
速度与效率:
- TCP 由于其可靠性机制,速度相对较慢,因为它需要时间来建立连接、确认数据包和进行重传。
- UDP 由于没有这些机制,通常更快,适用于对实时性要求较高的应用,如视频会议和在线游戏。
-
数据流控制:
- TCP 有流量控制和拥塞控制机制,可以根据网络状况调整数据传输的速度。
- UDP 没有这样的控制机制,发送速率不会因网络状况而改变。
-
用途:
- TCP 常用于要求高可靠性的应用,如网页浏览、电子邮件和文件传输。
- UDP 常用于实时应用,如流媒体、VoIP(网络电话)和在线游戏,这些应用对速度的要求高于数据完整性。
-
头部开销:
- TCP 头部较大,因为它需要包含更多的信息来保证数据的可靠传输。
- UDP 头部较小,处理起来更快,开销更小
这里只是简单介绍,看不懂没有关系,有个基础概念即可。
3、网络字节序
在我们机器中,分为大端机和小端机,当我们在网络进行传输时,可能会面临大端机向小端机传输数据的情况。此时接受方读取数据时就可能会出现异常,所以这里统一将网络的数据定为大端。但是每次都要我们手动对数据进行大小端的转换未免太过麻烦,所以系统为我们提供特定的接口统一转换,后续会对其进行介绍。
4、接口预备知识
socket编程,是有很多的种类,有的是专门用来本地通信的(Unix socket),有的是用来跨网络进行通信的(inet socket),有的是用来进行网络管理的(raw socket )。这些套接字类型非常多,为了减少学习的成本,linux编写者就决定让这些套接字使用统一的接口。而OS是由C语言进行编写的,涉及到统一类型的问题就必须和结构体相关联。其中有关套接字的结构体常见有三种。
我们常用的类型是struct sockaddr,但实际上我们用于存储数据的结构体是struct sockadd_in和struct sockaddr_un。与网络编程相关的接口都使用的是sockaddr结构体,所以我们需要先用sockaddr_in结构体存储数据,然后强转成sockaddr结构。这种特性和C++多态非常类似。这里我们着重关注sockaddr结构即可,当我们将sockaddr_in 结构强转成sockaddr后,相关的接口依然能够识别原来sockaddr_in结构体内的数据。(sockaddr_un同理)
5、相关接口的介绍与认识
1、socket
该函数用于创建套接字,第一个参数用于指定套接字的域,第二个参数是套接字的类型,第三个参数在前两个参数确定的情况下填零即可。该接口成功调用的返回值是一个文件描述符,失败就返回-1,网络套接字创建以后相当于绑定了该文件描述符。这里我们一般就使用AF_INET。
第一个参数列表
第二个参数列表(使用udp协议时,我们就需要将该参数设置成SOCK_DGRAM)
2、bind
在创建完套接字后,我们必须要将创建的套接字与端口号和ip地址进行绑定,也就是将网络服务与本地的文件描述符绑定(先这样理解)。所以这里我们需要引入一个接口bind
第一个参数表示创建套接字的文件描述符,第二个是网络套接字的相关结构体(就是我们上文所提到的sockaddr,不过实际上我们使用的是sockaddr_in),第三个参数表示第二个参数代表的结构体大小。
在使用struct sockaddr_in之前,我们首先要将其定义出来,初始化后再对各个成员进行初始化。
sin_family 我们一般初始化成AF_INET即可,而sin_port和sin_addr的初始化需要特别注意的是,这两个成员再初始化之前,我们都需要对其作主机序列转成网络序列的操作,也就是转成大端,除此之外,由于IP地址是点分十进制风格的字符串来表示的,所以在网络传输前,还要将其变成4字节的IP。在初始化IP地址时,我们发现sockaddr_in中,表示IP地址的成员是一个结构体,而这个结构体内只有一个成员,在初始化的时候需要注意以下。上述这些操作都不需要我们手动的实现,OS已经提供了相关的接口。
端口的主机序列转网络序列的相关接口(这里我们使用第二个接口,因为我们一般将端口设成16位)
当前IP的主机序列转网络序列并将其转成4字节IP的相关接口(这里我们一般使用第二个接口)
3、recvfrom
该接口用于接收网络中数据,第一个参数是服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,用于存放接收的数据,第三个参数用于表示接收数据的长度,第四个参数是位掩码,用于控制该函数,我们通常填零即可(因情况而定),第五个参数也是一个输出型的参数,用于接收发送方的信息,第六个参数用于表示第五个参数的大小。返回值表示实际读取到的数据长度。
4、sendto
该接口用于向特定主机发送数据,第一个参数服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,表示要发送的数据。第三个参数表示缓冲区的大小,第四个参数表示位掩码,表示对该函数的控制,一般设置为零即可,第五个参数表示目标主机的相关信息,第六个参数表示第五个参数的大小。返回值表示发送出去的数据长度。
示例代码(udp)
Main.cc(服务端)
#include <iostream>
#include <memory>
#include "Udpserver.hpp"
void Usage()
{
std::cout << "./Main.cc server_port\n"<< std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage();
return 0;
}
EnableScreen();
//std::string server_ip = argv[1];
int server_port = std::stoi(argv[1]);
std::unique_ptr<Udpserver> ptr = std::make_unique<Udpserver>(server_port);
ptr->InitServer();
ptr->Start();
return 0;
}
Udpserver.hpp
#pragma once
#include <iostream>
#include "Log.hpp"
#include <strings.h>
#include "Inetaddr.hpp"
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{
SOCKET = 1,
BIND
};
class Udpserver
{
public:
Udpserver(uint16_t port) : _port(port), _isrunning(false)
{
}
void InitServer()
{
_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (_fd < 0)
{
LOG(INFO, "socket fail")
exit(SOCKET);
}
struct sockaddr_in infor;
bzero(&infor, sizeof(infor));//清空数据
infor.sin_family = AF_INET;
// 主机序列转网络序列
infor.sin_port = htons(_port);
infor.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定
socklen_t len = (socklen_t)sizeof(infor);
int count = bind(_fd, (struct sockaddr *)&infor, len);
if (count < 0)
{
LOG(ERROR, "bind fail ...")
exit(BIND);
}
LOG(INFO, "bind success")
}
void Start()
{
_isrunning = true;
LOG(INFO,"begin server...")
while (1)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in src;
socklen_t len = (socklen_t)sizeof(src);
ssize_t rnum = recvfrom(_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&src, &len);
if (rnum > 0)
{
buffer[1023] = 0;
Inetaddr addr(&src);
LOG(INFO, "receive informaiton success")
printf("#[%s:%d]: %s\n",addr.IP().c_str(),addr.Port(),buffer);
ssize_t snum = sendto(_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&src, len);
}
}
_isrunning = false;
}
~Udpserver()
{
}
private:
int _fd;
bool _isrunning;
uint16_t _port;
//std::string _IP;
};
client.cc(客户端)
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include "Log.hpp"
#include <strings.h>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
using namespace std;
void Usage()
{
std::cout << "./Main.cc server_ip server_port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 0;
}
// 创建套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
{
LOG(FATAL, "socket fail...")
exit(-1);
}
struct sockaddr_in client;
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(stoi(argv[2]));
client.sin_addr.s_addr = inet_addr(argv[1]);
// client 不需要显示地绑定客户端。OS会在client发送数据时,随机绑定一个端口号
// 通信
std::string message;
while (1)
{
std::cout << "Please Enter: ";
std::getline(std::cin, message);
sendto(fd, message.c_str(), message.size(), 0, (struct sockaddr *)&client, sizeof(client));
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
memset(buffer, 0 , sizeof(buffer));
ssize_t n = recvfrom(fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
std::cout << buffer << std::endl;
}
}
return 0;
}
Ineraddr.hpp
#include<iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
class Inetaddr
{
private:
void Init()
{
port = ntohs(_src->sin_port);
ip = inet_ntoa(_src->sin_addr);
}
public:
Inetaddr(struct sockaddr_in* src):_src(src)
{
Init();
}
std::string IP()
{
return ip;
}
uint16_t Port()
{
return port;
}
~Inetaddr()
{
}
private:
struct sockaddr_in* _src;
std::string ip;
uint16_t port;
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <pthread.h>
#include <fstream>
enum Level
{
INFO = 0,
DEBUG,
WARNING,
ERROR,
FATAL
};
std::string Level_tostring(int level)
{
switch (level)
{
case INFO:
return "INFO";
case DEBUG:
return "DEBUG";
case WARNING:
return "ERROR";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return "Unkown";
}
}
pthread_mutex_t _glock = PTHREAD_MUTEX_INITIALIZER;
bool _is_save = false;
const std::string filename = "log.txt";
void SaveLog(const std::string context)
{
std::ofstream infile;
infile.open(filename,std::ios::app);
if(!infile.is_open())
{
std::cout << "open file failed" << std::endl;
}
else
{
infile << context;
}
infile.close();
}
std::string Gettime()
{
time_t cur_time = time(NULL);
struct tm *time_data = localtime(&cur_time);
if (time_data == nullptr)
{
return "None";
}
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d",
time_data->tm_year + 1900,
time_data->tm_mon + 1,
time_data->tm_mday,
time_data->tm_hour,
time_data->tm_min,
time_data->tm_sec);
return buffer;
}
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = Level_tostring(level);
std::string time = Gettime();
// 可变参数
char buffer[1024];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
std::string context = "[" + levelstr + "]" + "[" + time + "]" + "[" + "line : " + std::to_string(line) + "]" + "[" + filename + "]" + ": " + buffer;
pthread_mutex_lock(&_glock);
if(!issave)
{
std::cout << context << std::endl;
}
else{
SaveLog(context);
}
pthread_mutex_unlock(&_glock);
}
#define LOG(level, format, ...) \
do \
{ \
LogMessage(__FILE__, __LINE__, _is_save, level, format, ##__VA_ARGS__); \
} while (0);
#define EnableFile() \
do \
{ \
_is_save = true; \
} while (0);
#define EnableScreen() \
do \
{ \
_is_save = false;\
} while (0);
代码编写中及测试过程中的注意事项
<1>在客户端中,我们是不需要显示地绑定一个端口号的,操作系统会第一次连接时自动帮我们绑定一个随机端口号。如果帮显示绑定一个端口号,那么可能就会造成以下情况,当你的主机上需要同时启动两款App,但是这两款App绑定了同一个端口号,此时就会造成一个App启动后,另一个个App启动失败。
<2>当我们接收到sockadd_in结构体后,如果需要打印结构体内的信息,需要对对其进行从网络序列转成主机序列的操作,这也就是Inetaddr文件存在的原因(这里我封装了)。
<3>服务端不推荐绑定固定的ip(我们一般就绑定为0,表示能够处理任何ip发送的服务)。在云服务器上也不允许绑定公网的ip,如果需要在云服务器上绑定ip,则需要在对应的云服务的安全组上添加对应的端口。
以上就是所有内容