在学习套接字编程之前,我们需要先明确一个概念:我们进行的所有网络通信行为,本质上都可以理解为进程间通信,那么对于双方而言,就需要能保证,1.数据能到达自己的机器 2.数据能识别机器上指定的进程。我们使用 ip地址来标识互联网中唯一的一台主机,用端口号来标识该指定机器中进程的唯一性,则双方的(ip, port)就能够标识互联网中各自唯一的两个进程,这就是传说中的套接字。
如何理解端口号
前面我们提到了使用端口号来标识主机上的唯一一个网络进程,那么问题来了,操作系统中已经有了pid可以标识唯一一个进程了,为什么还需要端口号呢?
首先,为网络通信专门定义一个端口号能够实现进程管理模块和网络模块的解耦合,其次,端口号专门用来进行网络通信,而系统中不是所有的进程都需要进行网络通信。
已知一个端口号和一个进程相关联,一个端口号不能和多个进程关联,但一个进程可以和多个端口号关联
TCP协议和UDP协议
TCP协议属于可靠通信,为了保证数据传输的可靠性,需要进行一些处理,比如丢包重传、流量控制等等,比较复杂,速度也比较慢一些,UDP协议属于不可靠通信,由于不保证数据传输的可靠性,协议更加简单,速度也会更快些。两种协议都有适用的场景,没有高下之分。
网络字节序
我们知道接入互联网的主机各种各样,不同厂商生产的设备也不一样,则不同的计算机可能使用不同的字节序,有的使用大端字节序:数据的最高有效字节存放在字节序列的低地址处,有的使用小段字节序:数据的最低有效字节存放在字节序列的低地址处。
为了一致性,互联网协议规定网络字节序为大端,则要想通过网络传输数据,就必须将数据以网络字节序发送:将发送缓冲区的数据按内存地址从低到高的顺序发出,同样要以网络字节序接收:按内存地址从低到高的顺序保存。
为了方便用户发送数据时将主机字节序转化为网络字节序,接收数据时将网络字节序转化为主机字节序,网络协议提供了接口可以方便的进行转化。

socket编程接口
套接字有三种类型:原始套接字、域间套接字、网络套接字,其中原始套接字允许直接访问底层传输协议,可以自定义协议,需要管理员权限才能使用,适用于处理特定协议或开发网络工具的场景;而域间套接字允许不同进程通过网络协议进行通信,进程可以位于同一台主机上,适用于分布式应用和进程间传输大量数据的场景;网络套接字分为流式套接字tcp和数据包套接字udp,这两种套接字也分别对应不同的场景,具体我们后面会提到。
// 创建 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* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
网络编程有着不同的应用场景,理论上我们需要为每种场景都涉及一套编程接口,但设计者想要用一套接口来完成这些事情,于是就有了struct sockaddr
struct sockaddr {
sa_family_t sa_family; /* 地址族,如AF_INET, AF_INET6, AF_UNIX等 */
char sa_data[14]; /* 地址的其余部分,大小足以容纳IPv4地址 */
};
对于IPv4地址,使用 struct sockaddr_in,对于IPv6地址,使用 struct sockaddr_in6,对于UNIX域地址,使用 struct sockaddr_un,这实际上就是C风格的多态。
首先是创建套接字的接口:

可以看到这个函数运行成功的返回值实际上就是套接字的文件描述符,所以向网络中读写数据本质上就是向这个打开的文件读写数据!这样体现了linux下一切皆文件的思想。

接下来看参数:
domain指的是网络协议家族,用于表示这个套接字将用于哪种通信,目前学习阶段我们进行网络通信直接填AF_INET即可。

type有几种类型:面向数据报(udp)和面向字节流(tcp)等等,具体什么是数据报和字节流我们等谈到传输协议的时候再聊。

至于第三个参数protocol,在我们确认完前两个参数后,自然也就确定下来了,我们填0j即可。
再来是绑定套接字的ip和port的接口:

第一个参数sockfd就是我们创建的套接字的文件描述符,至于第二个参数addr,正如我们前面提到的,struct sockaddr相当于面向对象语言中的基类,我们对于不同的网络通信场景,我们需要使用不同的"派生类",第三个参数则是addr的大小。
udp套接字使用的收发数据接口:

这两个接口recvfrom和sendto我们放在一起讲,第一个参数自然就是本地套接字的文件描述符,第二个参数buf则是一段缓冲区,用于储存接受和发送的数据,第三个参数len则是缓冲区的大小,第四个参数flags我们通常设置为0,对于recvfrom函数,第五个参数实际上是输入输出型参数,用于保存发送数据的对方的ip和port等信息,而对于sendto函数,第五个参数就是目标主机的ip和端口等信息,第六个参数自然就是这个结构体的大小了。
学习过这几个接口后,我们就可以开始编写最简单的udp服务器的代码了,具体细节将在注释中标注出来。
编写简单udp服务器代码
这份udp服务器代码的逻辑非常简单,就是客户端从键盘输入数据然后发送给服务端,服务端将接收数据并把客户端发来的数据重新回显给客户端。
服务器类代码:Udpserver.hpp
#include <iostream>
#include <cstdint>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include "NoCopy.hpp" // 让我们的服务器类继承一个自己写的不可以拷贝的类,防止服务器被拷贝
#include "log.hpp" // 这是一个自己编写的日志类,用于更方便输出服务器信息
#include "common.hpp" // 存放枚举类
#include "InetAddr.hpp" // 将网络字节序转化为主机字节序
static const int defaultport = 8888;
static const int defaultfd = -1;
static const int defaultsize = 1024;
class UdpServer : public NoCopy
{
public:
UdpServer(uint16_t port = defaultport)
: _port(port), _sockfd(defaultfd)
{}
void Init()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket error, %d: %s\n", errno, strerror(errno));
exit(Sock_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);
// 为socket绑定网络信息
// 在本地填充好sockaddr_in结构体用于绑定
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 防止结构体内未定义信息造成影响
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 将主机字节序转化为网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 服务器的ip不应该直接绑定
// 将本地结构体与套接字进行绑定
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
lg.LogMessage(Fatal, "bind error, %d: %s", errno, strerror);
exit(Bind_Err);
}
}
void Start()
{
char buffer[defaultsize];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
}
~UdpServer()
{
}
private:
// 我们不让服务器绑定ip地址
uint16_t _port; // 服务器的端口号
int _sockfd; // 服务器使用的套接字
};
防止服务器被拷贝:NoCopy.hpp
#pragma once
// 我们不想让服务器被拷贝,于是让服务器类继承一个不可被拷贝的类
class NoCopy
{
public:
NoCopy(){}
NoCopy(const NoCopy &) = delete;
NoCopy &operator=(const NoCopy &) = delete;
~NoCopy(){}
};
日志类:log.hpp(不是特意写的,其实是以前的,顺手用上了)
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
enum
{
Screen = 10,
OneFile,
ClassFile
};
string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unknown";
}
}
const int defaultStyle = Screen;
const string defaultFileName = "log.";
const string logdir = "log";
class Log
{
public:
Log():_style(defaultStyle), _fileName(defaultFileName)
{
mkdir(logdir.c_str(), 0775);
}
// 支持把日志信息写入到其他位置的用户接口
// OneFile、ClassFile
void Enable(int style)
{
_style = style;
}
string StampExLocalTime()
{
time_t currtime = time(nullptr);
struct tm *time = localtime(&currtime);
char timeBuffer[128];
snprintf(timeBuffer, sizeof(timeBuffer), "%d-%d-%d %d:%02d:%02d",
time->tm_year + 1900, time->tm_mon+1, time->tm_mday, time->tm_hour, time->tm_min, time->tm_sec);
return timeBuffer;
}
void WriteToOneFile(const string &name, const string &message)
{
umask(0);
int fd = open(name.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) return;
write(fd, message.c_str(), message.size());
close(fd);
}
// 文件名类似于: log.Debug、log.Warning...
void WriteToClassFile(const string &levelstr, const string &message)
{
string name = logdir;
name += "/";
name += _fileName;
name += levelstr;
WriteToOneFile(name, message);
}
void WriteLog(const string &levelstr, const string &message)
{
switch(_style)
{
case Screen:
cout << message;
break;
case OneFile:
WriteToClassFile("all", message);
break;
case ClassFile:
WriteToClassFile(levelstr, message);
break;
default:
break;
}
}
// 我们希望提供一个类C的日志接口,并让函数支持可变参数列表
// Debug, Info, Warning, Error, Fatal
void LogMessage(int level, const char *format, ...)
{
char rightBuffer[1024];
va_list args;
va_start(args, format);
// 此时args指向了可变参数部分
vsnprintf(rightBuffer, sizeof(rightBuffer), format, args);
va_end(args);
char leftBuffer[1024];
string levelString = LevelToString(level);
string time = StampExLocalTime();
snprintf(leftBuffer, sizeof(leftBuffer), "[%s][%s] ", levelString.c_str(), time.c_str());
string logInfo = leftBuffer;
logInfo += rightBuffer;
WriteLog(levelString, logInfo);
}
~Log()
{
}
private:
int _style;
string _fileName;
};
Log lg;
初始化和启动服务器:Main.cc
#include "Udpserver.hpp"
#include <memory>
// 用户手册
void Usage(const string &proc)
{
std::cout << "Usage: \n\t" << proc << " local_port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
客户端代码:client.cc
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
void Usage(const std::string &proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket failed: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "socket success" << std::endl;
// 将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());
while (true)
{
// 用户从键盘输入信息
std::string inbuffer;
std::cout << "please enter# ";
getline(std::cin, inbuffer);
ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
char buffer[1024];
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}
通过这份代码,相信大家可以发现一件事,udp套接字不需要进行连接就能够直接进行发送信息,这就是所谓的无连接性,这也是udp套接字与tcp套接字的一个区别。
5850

被折叠的 条评论
为什么被折叠?



