文章目录

前言Socket编程基础概念工作原理

Socket API介绍socket函数绑定、监听函数accept、connect接受/发送函数

Socket API的应用Socket类与其派生类的设计服务器与客户端的设计使用

总结

前言

现今我们的日常生活当中,网络已经成为了必不可少的存在,大到覆盖全世界的互联网,小到身边的各种电器,可以说网络无处不在。我们作为一名程序员,如果对网络不甚了解,那么注定会度过一个相对失败的一生,需要利用网络进行通信的应用正变得越来越多,企业对程序员网络知识的需求也越发变得重要,因此,学习网络一定会对你有所帮助。

Socket编程基础

概念

Socket 的中文名可以译为套接字、插座,就像它的直译插座一样,Socket 是两台机器网络通讯的端点,只要使用Socket就能连接两台机器,从而实现数据的传输、交换。

在介绍Socket是如何工作前,我们需要先了解一下网络的基本术语。

基础术语 IP地址:IP地址是设备在网络上的标识符,要进行网络通信就必须拥有一个IP地址 端口:端口的设计是为了让网络数据正确发送到应用程序,计算机通过IP+端口号来确保数据收发正确。 协议:协议是定义数据如何在网络传输的规则,Socket编程中会接触到的协议有UDP、TCP协议。

工作原理

正如上方所说Socket是网络通信的端点,Socket的工作原理是基于C—S模型,即必定会有客户端与服务端的存在。既然要通信,那么就一定会有协议的存在。socket 有面向字节流协议的SOCK_STREAM、面向数据报的 SOCK_DGRAM 和 直接将数据发往IP层的原始套接字 SOCK_RAW。

其实 SOCK_STREAM 与 SOCK_DGRAM 就已经可以完成99%的网络通讯设计,毕竟现在网络上主流的协议也就是UDP和TCP协议。虽然协议本身区别很大,但在应用层的使用上,大体还是差不多的。

服务器端的工作流程

创建套接字。 绑定地址。 接受数据。 发送数据。 客户端的工作流程

创建套接字提前确定远端的地址、端口发送数据接受数据。

Socket API介绍

socket函数

socket函数是系统用于创建套接字描述符的接口,该函数会返回一个文件描述符,之后网络的通信便围绕着这个文件描述符进行。

#include

//函数原型

int socket(int domain, int type, int protocol);

// 返回值为文件描述符

int fd = socket(AF_INET, SOCK_STREAM, 0);

参数选项

domain: 用于指定通信域,常用的选项为AF_INET(指定使用IPV4通信),AF_INET6(指定IPV6通信),AF_UNIX(指定本地进程间通信)。type: 用于指定socket的类型。常用的选项为SOCK_STREAM(提供可靠的流传输服务,也就是TCP),SOCK_DGRAM(提供不可靠的数据报服务,也就是UDP)。protocl: 用于指定是否使用特殊协议,一般设为0。

绑定、监听函数

bind 函数用于让程序绑定一个固定的端口号,使套接字只从该端口号接受/发送数据,一般用于服务器显示绑定地址,客户端通过系统自动分配。listen 函数用于监听端口号,等待客户端的连接。

#include

int listen(int sockfd, int backlog); //成功返回0

int bind(int sockfd, const struct sockaddr *addr, //成功返回0

socklen_t addrlen);

/* socketaddr是C语言历史缘由而留下来的结构体,因为当初C语言还不支持

void* 类型,所以设计出了sockaddr类型,以应对不同的选项。 */

//以下是socketaddr家族

struct sockaddr { //基础类型

sa_family_t sa_family;

char sa_data[14];

}

struct sockaddr_in {

__uint8_t sin_len; //无特殊要求不会指定值

sa_family_t sin_family; //设置协议家族(如AF_INET、AF_UNIX)

in_port_t sin_port; //设置端口

struct in_addr sin_addr; //设置IP地址

char sin_zero[8];

};

//socket_in6 用于IPV6设置。

bind 参数选项

sockfd: socket 文件描述符。addr: 绑定socket_addr。addrlen: 指定socket_addr的长度。 listen 参数选项

sockfd: 指定需要监听的套接字。backlog: 用于指定套接字中处于排队TCP连接数(还未得到处理),用于防止 SYN 泛洪攻击。

accept、connect

accept 和 connect 这两个函数,它们一般用于TCP协议,因为UDP是无连接的所以用不上(connect除外)。

accept 函数用于接受一个TCP连接,并返回它的套接字描述符,之后的读写则往该套接字描述符进行。注意,使用前需要先建立好监听状态。

connect 函数用于连接一个远端的服务器,成功则返回0.

#include

#include

int

accept(int socket, struct sockaddr *address, socklen_t *address_len);

int //connect函数用于连接服务器

connect(int socket, const struct sockaddr *address, socklen_t address_len);

//UDP连接也可以使用connect函数,一般用于为UDP的套接字绑定一个固定的远端地址,从此该套接字就只能接受该地址的数据(过滤)。

accept 的参数选项

socket: 指定需要接受数据的套接字接口address: 该结构用于接收连接方的协议地址。如果不想要远端的信息,可以设null。address_len: 用于指定address的长度。 connect 的参数选项

socket: socket 文件描述符address: 指向存放目标服务器地址的信息。addlen: 指定addr结构体的长度。

接受/发送函数

unix like 系统中,UDP与TCP协议数据的收发所使用的函数有些许的差别,主要就是是否需要指定远端的地址、端口的差别,TCP方面因为已经通过 accpt 创建了一个包含远端信息的套接字,而UDP是无连接的,所以需要传入一个包含远端信息的sockaddr 结构体。

TCP协议所使用的接发函数:

#include

// recv send 参数都是一致的。

ssize_t recv(int sockfd, void buf, size_t len,

int flags);

ssize_t send(int sockfd, const void buf, size_t len, int flags);

参数选项:

sockfd: 指定远端的套接字接口buf: 需要接受/发送的数据len: 数据的长度flags: 可提供额外的控制选项,如指定阻塞等待(MSG_DONTWAIT)。

UDP协议所使用的接收/发送函数:

ssize_t recvfrom(int sockfd, void buf, size_t len,

int flags,

struct sockaddr * src_addr, //可设为空,但如果要发送数据则要存储该结构体

socklen_t * addrlen);

ssize_t sendto(int sockfd, const void buf, size_t len, int flags,

const struct sockaddr *dest_addr, socklen_t addrlen);

// 额外所需要用的

uint16_t

htons(uint16_t hostshort); //将主机序列转为网络序列,网络数据使用大端传送

unsigned long

inet_addr(const char *cp); //用于将字符串ip地址转为网络字节序的二进制形式

参数选项:

sockfd: 指定需要接收/发送数据的套接字。buf: 数据所存放的内存flags: 发送的选项,与TCP一样addr: 如果需要发送数据,则需要在recvfrom指定中存储的sockadd结构体addrlen: sockaddr 的长度,如 sockaddr_in 。

Socket API的应用

介绍完了基本的函数信息,也该到实践的环节了,但如果只是简单写下函数的使用方法,也并没有什么实际意义,那么不如构建一个Socket编程的模版,这样以来使用 Socket 编程就不必再敲重复的代码,而且也能提高对设计模式的理解。

代码的大致样子:

#mermaid-svg-vjkoT80FFEaNph6o {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-vjkoT80FFEaNph6o .error-icon{fill:#552222;}#mermaid-svg-vjkoT80FFEaNph6o .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vjkoT80FFEaNph6o .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-vjkoT80FFEaNph6o .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vjkoT80FFEaNph6o .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vjkoT80FFEaNph6o .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vjkoT80FFEaNph6o .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vjkoT80FFEaNph6o .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vjkoT80FFEaNph6o .marker.cross{stroke:#333333;}#mermaid-svg-vjkoT80FFEaNph6o svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vjkoT80FFEaNph6o g.classGroup text{fill:#9370DB;fill:#131300;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-vjkoT80FFEaNph6o g.classGroup text .title{font-weight:bolder;}#mermaid-svg-vjkoT80FFEaNph6o .nodeLabel,#mermaid-svg-vjkoT80FFEaNph6o .edgeLabel{color:#131300;}#mermaid-svg-vjkoT80FFEaNph6o .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-vjkoT80FFEaNph6o .label text{fill:#131300;}#mermaid-svg-vjkoT80FFEaNph6o .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-vjkoT80FFEaNph6o .classTitle{font-weight:bolder;}#mermaid-svg-vjkoT80FFEaNph6o .node rect,#mermaid-svg-vjkoT80FFEaNph6o .node circle,#mermaid-svg-vjkoT80FFEaNph6o .node ellipse,#mermaid-svg-vjkoT80FFEaNph6o .node polygon,#mermaid-svg-vjkoT80FFEaNph6o .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vjkoT80FFEaNph6o .divider{stroke:#9370DB;stroke:1;}#mermaid-svg-vjkoT80FFEaNph6o g.clickable{cursor:pointer;}#mermaid-svg-vjkoT80FFEaNph6o g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-vjkoT80FFEaNph6o g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-vjkoT80FFEaNph6o .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-vjkoT80FFEaNph6o .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-vjkoT80FFEaNph6o .dashed-line{stroke-dasharray:3;}#mermaid-svg-vjkoT80FFEaNph6o #compositionStart,#mermaid-svg-vjkoT80FFEaNph6o .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #compositionEnd,#mermaid-svg-vjkoT80FFEaNph6o .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #dependencyStart,#mermaid-svg-vjkoT80FFEaNph6o .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #dependencyStart,#mermaid-svg-vjkoT80FFEaNph6o .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #extensionStart,#mermaid-svg-vjkoT80FFEaNph6o .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #extensionEnd,#mermaid-svg-vjkoT80FFEaNph6o .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #aggregationStart,#mermaid-svg-vjkoT80FFEaNph6o .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o #aggregationEnd,#mermaid-svg-vjkoT80FFEaNph6o .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-vjkoT80FFEaNph6o .edgeTerminals{font-size:11px;}#mermaid-svg-vjkoT80FFEaNph6o :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

派生

派生

RemoteData

-sockaddr_in _addr

-std::string _data

-int _socket

«abstract»

Socket

-int _socket

-int _port

-std::string _ip

-sockaddr_in _addr

+virtual ~Socket()

+virtual bool BindSocket()

+virtual bool Accept(RemoteData* data)

+virtual bool ConnectSocket()

+virtual bool CreateSocket()

+virtual bool RecvData(RemoteData*)

+virtual bool SendData(RemoteData*)

+void BuildServer()

+void BuildClient()

UdpSocket

+bool CreateSocket()

+bool RecvData(RemoteData* remoteData)

+bool SendData(RemoteData* data)

TcpSocket

+bool CreateSocket()

+bool BindSocket()

+bool RecvData(RemoteData* remoteData)

+bool SendData(RemoteData* data)

Socket类与其派生类的设计

Socket 基类是TcpSocket、UdpSocket的抽象基类,用于提高代码的复用性。

#include

#include

#include

#define MAX_BUFFER_SIZE 1024

struct RemoteData //用于获取远端数据

{

public:

RemoteData() = default;

explicit RemoteData(sockaddr_in& client, int fd = -1)

:_addr(client), _socket(fd)

{

_data.resize(MAX_BUFFER_SIZE);

}

~RemoteData() = default;

sockaddr_in _addr;

std::string _data; // 改用 char buffer[];

int _socket = -1;

};

// 使用模版方法进行封装

class Socket

{

public:

Socket(std::string ip, int port)

:_ip(std::move(ip)), _port(port)

{}

void BuildServer() //用于服务器的构造

{

bool socket = CreateSocket();

bool bindSocket = BindSocket();

if(!(socket && bindSocket))

{

std::cerr << "socket build failed\n";

return;

}

std::cout << "socket build success\n";

}

void BuildClient() // 用于客户端的构造

{

bool socket = CreateSocket();

bool connectSocket = ConnectSocket();

if(!(socket && connectSocket))

{

std::cerr << "socket build failed\n";

return;

}

std::cout << "socket build success\n";

}

protected:

virtual bool BindSocket() // 用于绑定套接字

{

if(::bind(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) < 0)

return false;

return true;

}

virtual bool Accept(RemoteData* data) // 用于接受套接字

{

socklen_t len = sizeof (sockaddr_in);

sockaddr_in* addr = &data->_addr;

data->_socket = accept(_socket, (sockaddr*)addr, &len);

if(data->_socket < 0)

{

std::cerr << "accept failed " << strerror(errno) << std::endl;

return false;

}

return true;

}

virtual bool ConnectSocket() // 用于连接套接字

{

if(::connect(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) < 0)

return false;

return true;

}

virtual ~Socket() = default; // 基类继承需要把析构函数设为虚函数。

virtual bool CreateSocket() = 0; // 创建套接字

virtual bool RecvData(RemoteData*) = 0; // 接收数据

virtual bool SendData(RemoteData*) = 0; // 发送数据

protected:

int _socket=-1;

int _port{};

std::string _ip{};

sockaddr_in _addr{};

};

TcpSocket 和 UdpSocket

TcpSocket 与 UdpSocket 就如其名,对应了TCP与UDP的socket编程设计。

class UdpSocket : public Socket

{ // UdpSocket如果使用connect函数,可以使用send、recv来代替sendto、recvfrom

public:

UdpSocket(std::string ip, int port)

: Socket(std::move(ip), port) //初始化基类

{}

~UdpSocket() override = default;

protected:

bool CreateSocket() override

{

_addr.sin_family = AF_INET;

_addr.sin_port = htons(_port); //将主机字节序列转为网络字节序列

_addr.sin_addr.s_addr = inet_addr(_ip.c_str());

_socket = socket(AF_INET, SOCK_DGRAM, 0);

if(_socket < 0) return false; //错误处理

return true;

}

bool RecvData(RemoteData* remoteData) override

{

char* buffer = remoteData->_data.data();

socklen_t len = sizeof(sockaddr_in);

sockaddr_in* client = &remoteData->_addr;

ssize_t n = recvfrom(_socket, buffer, MAX_BUFFER_SIZE-1, 0, (struct sockaddr*)client, &len);

if(n == 0)

{

std::cout << "client close\n";

return false;

}

else if(n > 0)

{

buffer[n] = '\0';

std::cout << "recv data : " << buffer << std::endl;

return true;

}

else

{

std::cerr << "recvfrom error\n";

return false;

}

}

bool ConnectSocket() override

{

return true;

}

bool SendData(RemoteData* data) override

{

char* buffer = data->_data.data();

sockaddr_in* client = &data->_addr;

ssize_t n = sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr*)client, sizeof(*client));

if(n < 0)

{

std::cerr << "sendto error: " << strerror(errno) << std::endl;

return false;

}

return true;

}

};

class TcpSocket : public Socket

{

public:

TcpSocket(std::string ip, int port)

: Socket(std::move(ip), port)

{}

~TcpSocket() override = default;

bool CreateSocket() override

{

_addr.sin_family = AF_INET;

_addr.sin_port = htons(_port);

_addr.sin_addr.s_addr = inet_addr(_ip.c_str());

_socket = socket(AF_INET, SOCK_STREAM, 0); //使用SOCK_STREAM

if(_socket < 0) return false;

return true;

}

bool BindSocket() override

{

if(::bind(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) )

{

std::cerr << "bind failed\n";

return false;

}

listen(_socket, 5); //TCP服务器需要监听端口

return true;

}

bool RecvData(RemoteData* remoteData) override

{

int socket = remoteData->_socket;

char* buffer = remoteData->_data.data();

ssize_t n = recv(socket, buffer, MAX_BUFFER_SIZE-1, 0);

if(n == 0)

{

std::cout << "client close\n";

return false;

}

else if(n > 0)

{

buffer[n] = '\0';

std::cout << "recv data : " << buffer << std::endl;

return true;

}

else

{

std::cerr << "recv error\n";

return false;

}

}

bool SendData(RemoteData* data) override

{

int socket = data->_socket;

char* buffer = data->_data.data();

ssize_t n = send(socket, buffer, strlen(buffer), 0);

if(n < 0)

{

std::cerr << "send error\n";

return false;

}

return true;

}

};

服务器与客户端的设计

服务器设计

#include

#include

#include "Socket.hpp"

//#include "ThreadPool.hpp" //不懂线程池的可以去看看我写的线程池博客

class UdpServer : protected UdpSocket

{

public:

UdpServer(int port, std::function handler)

: UdpSocket("0.0.0.0", port), _handle(std::move(handler))

{

BuildServer(); //构建Socket

}

void Run(RemoteData* data)

{

_handle(data); //业务处理函数

SendData(data);

}

void start()

{

std::string msg;

while (true)

{

sockaddr_in client{};

std::shared_ptr data = std::make_shared(RemoteData(client));

if(!RecvData(data.get()))

continue;

// ThreadPool::GetInstance()->enqueue([this, data]{ Run(data.get());});

Run(data.get());

}

}

private:

std::function _handle; //业务处理函数

};

class TcpServer : TcpSocket // 注意:这个TCP协议需要进行粘包处理。

{

public:

TcpServer(int port, std::function handler)

: TcpSocket("0.0.0.0", port), _handle(std::move(handler))

{

BuildServer();

}

void ThreadRun(RemoteData* data)

{

while (true)

{

if(!RecvData(data)) break;

_handle(data);

if(!SendData(data)) break;

}

close(data->_socket);

}

void start()

{

std::string msg;

while (true)

{

sockaddr_in client{};

std::shared_ptr data = std::make_shared(RemoteData(client));

if(!Accept(data.get()))

break;

// ThreadPool::GetInstance()->enqueue([this, data]{ ThreadRun(data.get());}); //最好使用多线程进行业务处理,否则将只能处理一条连接

ThreadRun(data.get());

}

}

private:

std::function _handle;

};

客户端设计

class UdpClient : public UdpSocket

{

public:

UdpClient(std::string ip, int port, std::function func)

: UdpSocket(std::move(ip), port), _func(std::move(func))

{

BuildClient(); //

}

void start()

{

while (true)

{

std::shared_ptr data = std::make_shared(RemoteData(_addr));

_func(data.get());

SendData(data.get());

RecvData(data.get());

}

}

private:

std::function _func;

};

class TcpClient : public TcpSocket

{

public:

TcpClient(std::string ip, int port, std::function func)

: TcpSocket(std::move(ip), port), _func(std::move(func))

{

BuildClient();

}

void start()

{

while (true)

{

std::shared_ptr data = std::make_shared(RemoteData(_addr, _socket));

_func(data.get());

SendData(data.get());

RecvData(data.get());

}

}

private:

std::function _func;

};

使用

//client.cpp

#include "Client.hpp"

void handler(RemoteData* data)

{

std::cout << "client: ";

std::cin >> data->_data;

}

int main()

{ // 使用本地环回进行通信

UdpClient client("127.0.0.1", 8888, handler);

client.start();

return 0;

}

//server.cpp

#include "Server.hpp"

void handler(RemoteData* data)

{}

int main() {

ThreadPool* pool = ThreadPool::GetInstance(5);

UdpServer server(8888, std::function(handler));

server.start();

return 0;

}

总结

学习Socket编程只是迈入网络编程的第一步,计算机网络中还有TCP、UDP协议、IP协议等各种难关来等着我们来一一攻破。虽然你可能觉得学习Socket编程对学习TCP/IP协议这些没什么帮助,学校的老师也从来不会从代码开始攻坚计算机网络,但计算机网络就应该自顶至下,从应用层的应用开始学起。

博客主页:主页 我的专栏:C++ 我的github:github

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。