【Linux】网络套接字编程
前言
在掌握一定的网络基础,我们便可以先从代码入手,利用UDP协议/TCP协议进行编写套接字程序,明白网络中服务器端与客户端之间如何进行连接并且通信的。

目录
一、了解源目的IP、端口、网络字节序、套接字
端口号:
套接字:
认识传输层上的TCP/UDP协议:
网络字节序:
常见的套接字:
二、UDP网络编程
1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。
socket
bind
本地字节序和网络字节序相互转化
recvfrom&recv&recvmsg
send&sendto&sendmsg
客户端:
127.0.0.1本地回环地址
2.0 windows客户端向Linux服务器发送消息,服务器将此消息返回给对应的客户端。
三、TCP网络编程
1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。
listen
accept
connect
一、了解源目的IP、端口、网络字节序、套接字
首先,我们知道两个主机进行网络通信,那么就必须需要一个源IP和一个目的IP。根据此IP(这里IP就认为是公网IP,在特定的区域保证了其唯一性),能够确定在全网的一个唯一性。
在如下主机之间进行通信,我们理解一下通信的目的是什么:
通常情况下,把数据送到对方的机器是目的吗?可以发现,机器只是充当工具,是机器上的软件在进行通信。所以真正的网络通信过程本质就是进程间通信!将数据在主机间转发仅仅是手段。机器收到后,交付给指定的进程!
因为牵扯到了进程,当其中一个主机接收到网络信息之后,通过解包如何能够执行对应存在内存中的网络进程呢?这就和端口号相关了。
端口号:
传输层协议的内容。是标识特定主机上的网络进程的唯一性!
为何是网络进程的唯一性呢?首先是主机IP在全网的唯一性+进程在此主机里面的唯一性。所以此组合在全网就是唯一的进程。
并且端口号和进程id做了区分,解耦-进程id就管理系统的那一套,端口号就管网络的这一套。对于进程来说,一个进程可以绑定多个端口号(即存在不同的IP对其进程进行通信),但是端口号只能对于一个进程。(否则就确定不了唯一性了)
当然,既然IP存在源IP和目标IP,对于端口号来说也存在源端口号和目的端口号,表示是谁发的,谁来接收。
套接字:
实际上,我们将源IP和源端口号组成一起,目标IP和目标端口号组合在一起,这就是套接字。
Socket = {IP: 端口号};
端口号是16位。
认识传输层上的TCP/UDP协议:
| UDP:用户数据报协议 |
|---|
| 连接:无连接 |
| 可靠:不可靠传输(存在丢包等问题) |
| 面向数据报 |
| TCP:传输控制协议 |
|---|
| 连接:有连接 |
| 可靠:可靠传输 |
| 面向字节流 |
可靠指的是中性描述。出现丢包问题,在有些场景下容忍不可靠的。可靠性是需要大量的编码和处理的。UDP只是把数据发出了,更加简单。---选择协议的时候,根据场景需求:比如直播、视频网站适合使用UDP协议去做。
网络字节序:
我们知道,在计算机内存中保存数据时,根据高位和低位与地址的高位低位存在区别-即大小端字节序。由于在网络通信的过程中,我们们并不知道通信的两台主机究竟是否一样的字节序,如果存储字节序相反,那么另一台主机读取数据的时候就会出错。
所以,网络规定:所有从本地传输到网络上,必须是大端字节序。这样的话无论是以小端存储的机器还是大端存储的机器,每次从网络获取到的数据就一定是大端字节序,这样就可以方便进行转化读取就不会出现问题了。
常见的套接字:
1.域间socket 命名管道-类似
2.原始socket 编写很多工具 - 饶过很多上层协议使用底层。3.网络socket
理论上是三种应用场景,对应的是三套接口。但是Linux不想设计过多的接口所以将所有的接口统一。
并且为了管理套接字内的内容,定义了sockaaddr结构体,一个通用(所有关于套接字接口统一的类型),另外是分别针对不同套接字不同的结构体,方便相互转化,统一接口的使用。

sockaddr结构:
网络套接字:标志类型 16位端口 32IP地址 _in AF_INET // PF_INET
域间套接字:标志类型 108byte路径名 _un AF_UNIX
通用:前两个字节:标志类型 sockaddr
....
二、UDP网络编程
初识了上面的预备内容,接下了通过写代码的过程,将UDP协议网络编程的接口介绍,并且能够真正的做出客户端和服务器端进行简单聊天通信的过程:
1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。
服务器端:
我们对Server服务器端进行一个封装,封装为.hpp文件,我设想能够对其服务器进行初始化,启动两个步骤。
UDPServer.hpp
首先确定成员属性。对于socket编程来说,首先不可缺少的属性就是socket即套接字,实际上就是一个类似文件描述符的东西(fd),类型为int即可。其次就是ip和端口了。注意ip是16位整数,ip为32位。但是ip通常用点分十进制进行表示,用.分隔的每部分数字均为1字节,无符号的话那么表示就是0~255。
// 服务器封装
class UDPServer
{//......
private:// 源套接字 = 源ip + 源端口std::string _SRCip;uint16_t _SRCport;// UDP对应一套读写套接字对应文件描述符int _socket;
};
然后就是构造函数,构造函数需要对这些属性进行初始化。下述代码为什么设置ip默认参数为空,下面运行代码里会说明。
class UDPServer
{
public:UDPServer(uint16_t port, std::string ip = ""): _SRCip(ip), _SRCport(port), _socket(-1){}
//......
};
然后就是初始化。对于UDP协议服务器来说,初始化首先我们需要将我们创建的套接字与本地传入的ip以及端口号进行绑定。此时涉及到网络接口调用,这里我们一个一个介绍:
首先创建套接字:
socket
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol);
函数介绍:
socket() creates an endpoint for communication and returns a descriptor.(Socket()为通信创建一个端点并返回一个描述符。)
domain:此参数指定通信域;这将选择用于通信的协议族。(IPV4 - AF_INET(IPV6就是后面加个6))
type:指定的类型,该类型指定通信语义。UDP为SOCK_DGRAM - 数据报 TCP为SOCK_STREAM流式。
protocol:协议指定套接字要使用的特定协议。实际通过前两个参数选择就能自动推导出了,设置为0即可。
返回值:如果成功,则返回新套接字的文件描述符。如果出现错误,则返回-1,并适当地设置errno。
创建完套接字后,我们需要对本地IP和端口进行绑定,使用接口bind:
bind
头文件:
#include <sys/socket.h>
函数原型:
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
函数介绍:
socket:套接字文件描述符。
address:套接字结构体:
对于网络来说,需要使用结构struct sockaddr_in,之后传参的时候进行强转struct sockaddr即可。
struct sockaddr_in包含的内容属性如下:
sin_family - 协议家族 sin_port - 端口 sin_addr.s_addr - ip
对于ip,服务器一般设置为0.0.0.0 即0,这表示此服务器可以接收任何ip(因为一个服务器可能存在多个网卡的,让服务器在工作过程中可以从任意IP中获取数据)也推荐如此绑定,设置宏即可INADDR_ANY。
需要注意的是协议家族和上面的指定通信域一致,另外port和s_addr因为要发送到网络里去,所以需要将本地字节序转化为网络字节序。另外在初始化前,需要对其进行清零操作,可以使用函数memset或者bzero清零。
address_len:套接字结构体的大小。
返回值:成功完成后,bind()将返回0;否则,将返回-1,并设置errno表示错误。
因为在初始化套接字结构体的时候,要对属性转为网络字节序,有如下接口提供选择:
本地字节序和网络字节序相互转化
整型转化:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//将无符号整数(32字节)hostlong从主机字节序转换为网络字节序。
uint16_t htons(uint16_t hostshort);//将无符号短整型(16字节)hostshort从主机字节序转换为网络字节序。
uint32_t ntohl(uint32_t netlong);//将无符号整数(32字节)netlong从网络字节序转换为主机字节序。
uint16_t ntohs(uint16_t netshort);//将无符号短整型(16字节)netshort从网络字节序转换为主机字节序。
上述接口一般用于端口转化中,对于ip由于常用是字符串进行表示,也有相应接口进行转化为数字以及网络字节序。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);
//将互联网主机地址cp从IPv4的数字点表示法转换为二进制形式(按网络字节序),并将其存储在inp指向的结构中。inet_aton ()如果地址有效则返回非零,否则返回零。
in_addr_t inet_addr(const char *cp);
//将互联网主机地址cp从IPv4的点号表示法转换为网络字节序的二进制数据。如果输入无效,INADDR_NONE(通常是-1)就是返回。使用这个函数会有问题,因为-1是有效的地址(255.255.255.255)。应避免使用它,而应使用inet_aton()、inet_pton(3)或getaddrinfo(3),它们提供了一种更简洁的方式表示错误返回。
in_addr_t inet_network(const char *cp);
// 将cp (IPv4数字点表示法的字符串)转换为主机字节序的数字,以便用作Internet的网络地址。若成功,则依此返回地址。如果输入无效,则返回-1。
char *inet_ntoa(struct in_addr in);
//将以网络字节顺序给出的互联网主机地址转换为IPv4点分十进制记数法表示的字符串。字符串被返回到一个静态分配的缓冲区中,那些后续调用将被覆盖。
struct in_addr inet_makeaddr(int net, int host);
//是inet_netof()和inet_lnaof()的逆函数。它以网络字节顺序返回Internet主机地址,由网络编号net和创建本地地址主机,均按主机字节顺序。
in_addr_t inet_lnaof(struct in_addr in);
//返回的是Internet地址中的本地网络地址。返回值按主机字节顺序排列。
in_addr_t inet_netof(struct in_addr in);
//函数的作用是:返回Internet地址中的网络号部分。返回值按主机字节顺序排列。
如此,利用上述接口,我们就可以将套接字与服务器ip和地址进行一个绑定。
class UDPServer
{
public:
//......
// UDP服务器初始化:创建套接字+绑定void initserver(){// 创建套接字_socket = socket(AF_INET, SOCK_DGRAM, 0); // 网络-IPV4 面向数据报 协议-填0即可,会根据前面两个选项进行判断if (_socket < 0){// 返回-1表示创建套接字失败,致命错误logMessage(FATAL, "套接字创建失败-%d:%s", errno, strerror(errno));exit(1);}// 绑定本地进程struct sockaddr_in local; // 注意头文件必须包含完 man in_addrmemset(&local, 0, sizeof local);local.sin_family = AF_INET; // 协议家族local.sin_port = htons(_SRCport); // 注意,此处是要发送到网上的,需要转化为网络字节序,使用接口 hton 本地转网络 s是2字节16位local.sin_addr.s_addr = _SRCip.empty() ? INADDR_ANY : inet_addr(_SRCip.c_str()); // 如果为空,设置默认ip,此时可以接收任意ip发送的消息,不局限于一个ip。// 上述套接字结构初始化完毕,现在进行绑定if (bind(_socket, (struct sockaddr *)&local, sizeof local) < 0){// 小于零绑定失败!logMessage(FATAL, "绑定失败-%d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "UDP服务器初始化成功..... %s", strerror(errno));// UDP无连接 -初始化完成-}
//......
};
注意上述日志(logMessage)利用的是以前我博客中的log.h文件编写,可以利用print或者cout代替。
初始化完成后,我们需要编写服务器启动函数。由于UDP协议无需要连接,利用数据报传输,所以利用如下接口进行发送和接收目标主机信息即可:
recvfrom&recv&recvmsg
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
函数介绍:
recvfrom()和recvmsg()用于从套接字中接收消息,无论套接字是否面向连接,都可以使用它们接收数据。通常用于UDP协议套接字编程。
recv()调用通常只在已连接的套接字上使用(参见connect(2)),它与recvfrom()带有NULL src_addr参数相同。通常用于TCP协议套接字编程。
sockfd为对应套接字文件描述符,buf均为接收信息所存储的缓冲区,len为缓冲区的大小,flags为读取的方式,一般设置为0为阻塞读取。src_addr为传输信息的主机的套接字结构,addrlen是输入输出参数,输入需要传入原本套接字结构体的大小,返回就是返回后的套接字结构大小。
返回值:这些调用返回接收到的字节数,如果发生错误则返回-1。如果发生错误,则设置errno来指示错误。当对端执行完毕时,返回值为0有序关闭。
特别的,对于TCP协议中(recv、read),如果返回值>0就是正常读取,当返回值等于0的时候,说明对端已经关闭了连接了,当返回值小于0的时候,说明读取失败,设置错误码。
send&sendto&sendmsg
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
函数介绍:
send()调用只能在套接字处于连接状态时使用(以便知道预期的接收者)。TCP协议套接字编程使用。
sendto一般用于UDP协议套接字编程。
其中sockfd为套接字文件描述符,buf为要发送的存储区,len为此区大小,flags通常设为0,dest_addr为要发送的给目标主机的套接字结构体,addrlen就是此结构体的大小。
返回值:成功时,这些调用返回发送的字符数。如果出现错误,则返回-1,并适当地设置errno。
服务器启动函数,首先明确服务器是一个常驻进程,那么必然是死循环,并且不断接收不同客户端对其发送的消息。这里要求时将发送的消息打回去。可以利用接收到的目标主机的port和ip对其内容在服务器端显示,然后将原数据进行返回即可。
class UDPServer
{
public:
//......// UDP服务器通信开始!void start(){// 正式启动UDP服务器// 1.0版本 UDP接收客户端信息,返回给客户端本身消息char buffer[1024];while (true) // 常驻进程,永远不退出{// 创建客户端套接字结构,用来接收struct sockaddr_in client;socklen_t clientlen = sizeof(client);bzero(&client, sizeof client); // 清零空间// 面向数据报接收消息ssize_t n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &clientlen); // 0阻塞读取if (n > 0){buffer[n] = '\0';// 可以把对应主机的套接字信息提取出来std::string clientip = inet_ntoa(client.sin_addr); // 网络 转化ipuint16_t clientport = ntohs(client.sin_port);printf("[%s: %d]# %s\n", clientip.c_str(), clientport, buffer);}// 发送对应主机sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&client, sizeof(client));}}
//......
};
析构函数释放掉套接字内存即可,需要注意判断是否初始化,可以利用默认给的套接字的值进行判断。
class UDPServer
{
public:
//.......~UDPServer(){if (_socket != -1)close(_socket);}
//.......
};
UDPServer.cpp
然后,我们在源文件中,利用命令行参数,对服务器进行挂接启动即可。
#include "UDPServer.hpp"
#include <iostream>
#include <memory>static void UserManual()
{std::cout << "please:./UDPServer port" << std::endl;
}int main(int arc, char* argv[])
{if (arc != 2){UserManual();exit(-1);}std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(atoi(argv[1])));UDPServer_ptr->initserver();UDPServer_ptr->start();return 0;
}
客户端:
客户端使用接口上面已经介绍完毕,下面不在进行重复介绍。
客户端,首先自然需要获取服务器的ip和对应端口,然后给服务器端创建套接字结构使用接口sento发送过去即可。
那么我们这里需要想一下问题:客户端的套接字是否要绑定本地ip和端口呢?一般对于客户端,实际上就是应用。如果应用每次绑定特定的端口的话,根据之前所了解的网络基础我们可以知道,端口对于唯一的一个进程,那么不同的应用编写的时候很有可能存在绑定一样的端口,那么此时使用就会存在问题,即一个端口就对应了不同的进程了,服务器端返回数据不知道发送给谁。
所以,客户端套接字不需要自己绑定ip和端口,在发送给服务器首次的时候,便由操作系统随机安排,那么这样就可以避免端口号冲突的问题。
客户端不再进行封装,简易代码如下:
#include "log.hpp"
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>static void UserManual()
{std::cout << "please:./UDPClient ip port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3){UserManual();exit(-1);}// 首先创建套接字int sock = socket(AF_INET, SOCK_DGRAM, 0);// 注意,此套接字不可显示绑定 - 不能绑定确定的port// 将服务器端套接字结构初始化好struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr(argv[1]); // 首先点分式转化为数字然后转化为网络字节序保存起来char buffer[1024];while (true){std::cout << "请输入# ";std::string message;std::getline(std::cin, message);sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);// 接收消息struct sockaddr_in _server;socklen_t server_len = sizeof(_server);bzero(&_server, server_len);ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&_server, &server_len);if (s > 0){buffer[s] = '\0';std::cout << "server echo# " << buffer << std::endl;}}return 0;
}
我们在Linux下运行代码尝试:
127.0.0.1本地回环地址
首先进行本地测试,本地测试意思就是给服务器绑定ip 127.0.0.1,它是本地回环地址(loopback address),即不会上传到网络,而是在本机内测试,同样的本地协议栈还是会跑一遍,但是不会经过网络接口。
这样客户端和服务器首先本地跑一遍排查问题后就可以接入网去跑了,但是注意需要简单修改一下上面服务器写的命令行参数的处理。
// 在文件UDPServer.cpp 服务器源文件内修改
int main(int arc, char* argv[])
{int port;std::string ip = "";if (arc == 2){port = atoi(argv[1]);}else if (arc == 3){port = atoi(argv[2]);ip = argv[1];}else{UserManual();exit(-1);}std::unique_ptr<UDPServer> UDPServer_ptr(new UDPServer(port, ip));UDPServer_ptr->initserver();UDPServer_ptr->start();return 0;
}
测试效果如下:

本地测试成功,那么转入网络在来试一次:
(打马赛克的地方是自己的服务器公网ip)
在Linux环境下测试后,我们可不可以与windows系统上的进行通信呢?
2.0 windows客户端向Linux服务器发送消息,服务器将此消息返回给对应的客户端。
实际上,在不同的操作系统环境下,虽然有些系统调用变量,但是对于socket套接字编程来说对于网络就是一套的。对于UDP协议来说,windows客户端相对于Linux客户端(或者说windows下的网络编程)多了如下的初始化:
#include <WinSock2.h> // 引入套接字编程库 windows为此库 只需引入一个库就可以了#pragma warning(disable:4996) // 屏蔽错误 一般用一些不安全的函数,可以利用此进行屏蔽
#pragma comment(lib, "ws2_32.lib") // 固定用法,加载入库int main()
{WSADATA WSAData; // win 初始化if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){//WSACleanup();std::cout << "初始化失败!" << std::endl;return -1;}// ......closesocket(sock);WSACleanup(); // 结束return 0;
}
其余基本不变。

可以看到,windows端依然是可以向Linux端的服务器发送信息的。但是使用UDP协议进行网络通信的话,发送中文会存在乱码问题,需要转码解决。
根据上述的两个UDP代码操作实例,我们可以发现问题:
1.首先UDP协议的套接字文件描述符是既可以写也可以读,说明UDP协议的套接字文件描述符是全双工的。
2.UDP协议并没有任何连接,只要创建和初始化套接字相关信息后就直接进行了通信。
我们看看接下来的TCP协议和UDP协议编写的网络有什么不同,并且对于TCP服务器端使用一些功能让我们的网络服务更加复杂和形象化。
三、TCP网络编程
1.0 客户端向服务器发送消息,服务器将此消息返回给对应的客户端。
首先,还是从最基础的开始。
TCPServer.hpp
首先还是将TCP服务器进行封装,完成以下的两个功能:1.初始化 2.启动。
针对于UDP服务器的编码,TCP在编码实现上在创建服务器套接字,绑定服务器ip和端口后,UDP是初始化完了的,但是注意TCP协议与UDP协议一个最重要的区别就是有无连接。TCP是有连接的,所以一般就需要区分套接字类型。
好比一家饭店,店主专门派一个成员在马路边喊客人,当此成员(后面称为A成员)将客人喊道后,就交给店内的服务员去处理,然后A成员就继续去喊客去了。TCP的套接字实现实际上就是这样,分为两个套接字,一个监听套接字,一个处理或者可以称为服务套接字。监听套接字利用接口返回服务套接字的文件描述符,里面就已经绑定了客户端的套接字相关信息,并且因为是支持字节流的,所以也能通过read和write等文件操作函数进行操作。
那么在初始化的时候,最后需要设置监听套接字为监听模式,此时方为初始化完毕。
listen
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
int listen(int sockfd, int backlog);
函数介绍:
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2)(listen() 将 sockfd 引用的套接字标记为被动套接字,即作为将用于使用 accept(2) 接受传入连接请求的套接字)
sockfd:套接字文件描述符。
backlog:backlog 参数定义了 sockfd 的挂起连接队列可能增长到的最大长度。 如果连接请求在队列已满时到达,客户端可能会收到一个 带有 ECONNREFUSED 指示的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后重新尝试连接成功。
返回值:成功时,返回零。 出错时返回 -1,并适当设置 errno。
启动的话,对于UDP协议来说,是直接通信的,因为接收比如recvform存在客户端套接字结构体的相关信息。但是对于TCP来说,此时首先需要监听套接字在监听模式下,与当前服务器端进行连接的客户端进行连接,返回一个服务套接字,此时对于此服务套接字就可以对客户端的信息进行提取或者发送了。也就是说此时就连接上了一个客户端,而监听套接字返回之后就继续去监听去了,直到监听到又会继续返回。
accept
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数介绍:
accept() 系统调用与基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)一起使用。 它提取挂起连接队列中的第一个连接请求以进行监听,套接字sockfd创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。 新创建的套接字没有处于监听状态。 原来的socket不受此调用的影响。
sockfd:监听套接字
addr:输出型参数,接收客户端的套接字结构体。
addrlen:输入输出型参数,首先输入addr的大小,返回是返回接收客户端的套接字结构体大小。
返回值:正确返回一个非负整数,如果错误返回-1并且设置errno。
通过上面接口的介绍,我们就可以基本进行封装一下,因为1.0版本是只是单纯的服务器端返回给客户端数据,我们先实现下面的单进程或者线程版本:
#ifndef _TCPSERVER_
#define _TCPSERVER_#include "log.hpp"
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>const static int gbacklog = 20; // 一般不能设置太大和太小 -后面再说static void handleTheClient(int &serviceSock, const std::string &clientIp, uint16_t clientPort)
{char buffer[1024];while (true){printf("[%s: %d]# ", clientIp.c_str(), clientPort);// 接收的话注意是字节流,所以以前的文件那一套可以使用的ssize_t n = recv(serviceSock, buffer, sizeof(buffer) - 1, 0); // 阻塞等待,没有from就是适合面向字节流的,即TCP的,但是UDP写可以用的if (n > 0){buffer[n] = '\0';std::cout << buffer << std::endl;// 然后发送// 可以使用以前文件操作那一套或者send// send(serviceSock, buffer, sizeof(buffer) - 1, 0); // 阻塞发送,使用处理套接字ssize_t s = write(serviceSock, buffer, strlen(buffer));if (s < 0){logMessage(ERROR, "发送信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);break;}}else if (n == 0){// 对方关闭了连接,这里也进行关闭printf("[%s: %d]客户端关闭连接,我也关闭此连接\n");close(serviceSock);serviceSock = -1; // 防止后面析构再次释放break;}else{// 小于零说明接收失败logMessage(ERROR, "接收信息失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);close(serviceSock);break;}}
}class TCPServer
{
public:TCPServer(uint16_t port, std::string ip = "") : _server_ip(ip), _server_port(port),_listen_sock(-1), _service_sock(-1){}void initTCPServer(){// 初始化TCP服务器_listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 流式套接 自动识别为TCP协议,面向字节流if (_listen_sock < 0){logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(1);}// 服务器将自己的ip和端口绑定到对应的套接字上。struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof server_addr); // 初始化为0server_addr.sin_family = AF_INET; // 家族还是网络IPV4server_addr.sin_port = htons(_server_port); // 转化为网络字节序的端口号if (_server_ip.empty())server_addr.sin_addr.s_addr = INADDR_ANY; // 如果是空字符串就如此处理else{int n = inet_aton(_server_ip.c_str(), &server_addr.sin_addr); // 直接写入结构中,将点分式的ip地址转化为数字然后转化为网络字节序if (n == 0){logMessage(FATAL, "写入ip地址无效!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(2);}}// bindif (bind(_listen_sock, (struct sockaddr *)&server_addr, sizeof server_addr) < 0){logMessage(FATAL, "服务器端ip与port与套接字绑定失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(3);}logMessage(NORMAL, "绑定成功!......");// UDP到这一步初始化完毕,但是TCP还存在一步,需要进行连接// 因为TCP是面向连接的,我们正式通信前需要先建立连接// 此时就相当于设置_sock套接字为监听模式了if (listen(_listen_sock, gbacklog) < 0){logMessage(FATAL, "连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(4);}}// 初始化完就是启动了void start(){// 服务器先接收消息,然后在发送消息// TCP协议能否简单的像UDP那样直接进行通信吗?显然不能,在连接阶段使用的套接字是监听套接字,对信息处理并且发送是处理套接字所要干的事情struct sockaddr_in clientAddr;socklen_t clientAddrLen = sizeof clientAddr; // 用来接收客户端信息的套接字结构体while (true){// 首先确保常驻// 首先获取连接,连接我返回,不连接在这里进行阻塞_service_sock = accept(_listen_sock, (struct sockaddr *)&clientAddr, &clientAddrLen);if (_service_sock == -1){logMessage(ERROR, "连接客户端失败,重新连接... %d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);continue;// 连接失败不是致命的错误,重连一次即可}logMessage(NORMAL, "客户端连接成功.....");// 现在使用堆客户端信息处理的代码即可,这里我将他们封装为一个函数// 首先在外面先获取客户端的ip和端口std::string clientIp = inet_ntoa(clientAddr.sin_addr); // 将网络字节序的网络地址ip转化为点分十进制的字符串uint16_t clientPort = ntohs(clientAddr.sin_port); // 网络转为本地字节序,注意是16位整数// 处理信息handleTheClient(_service_sock, clientIp, clientPort);}}~TCPServer(){if (_listen_sock != -1)close(_listen_sock);if (_service_sock != -1)close(_service_sock);}private:std::string _server_ip;uint16_t _server_port;int _listen_sock; // 监听套接字int _service_sock; // 处理套接字
};#endif
TCPServer.cpp
然后在此文件内调用创建即可。
#include "TCPServer.hpp"
#include <memory>static void UserManual()
{std::cout << "please:./TCPServer ip port or /TCPServer port" << std::endl;
}int main(int argc, char* argv[])
{std::string ip = "";uint16_t port;if (argc == 2){port = atoi(argv[1]);}else if (argc == 3){ip = argv[1];port = atoi(argv[2]);}else{UserManual();exit(-1);}std::unique_ptr<TCPServer> TCP_server(new TCPServer(port, ip));TCP_server->initTCPServer();TCP_server->start();return 0;
}
TCPClient.cpp
对于客户端来说,首先我们不对其进行封装。
在UDP中我们已经介绍过,由于客户端的特殊性,所以让操作系统默认的帮我们绑定ip和端口,所以客户端只需要创建好套接字,以及初始化好服务器端的套接字结构体即可。上述步骤依旧一致,但是从此时开始UDP就直接可以接收了-数据报、无连接。
TCP是基于连接、字节流的,所以自然客户端首先需要和服务器端建立连接。(三次握手) 在建立连接后,此时客户端的套接字就可以正常的与服务器端进行通信了。此时可以将客户端的套接字视为一个普通的文件描述符,并且是可读可写,所以文件操作就可以一起用上了。
当然,客户端最后和服务器端需要断开连接。(四次挥手)
下面我们简单介绍一下客户端连接服务器端的接口函数:connect
connect
头文件:
#include <sys/types.h>
#include <sys/socket.h>函数原型:
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);函数介绍:
connect() 系统调用将文件描述符 sockfd 引用的套接字连接到 addr 指定的地址。
sockfd:客户端套接字
addr:服务器端套接字结构体
addrlen:服务器端套接字结构体大小
返回值:连接成功返回0,否则返回-1。
#include "log.hpp"
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <errno.h>static void UserManual()
{std::cout << "please:./TCPclient server_ip server_port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3){UserManual();exit(-1);}// 创建客户端的套接字int clientSock = socket(AF_INET, SOCK_STREAM, 0);if (clientSock < 0){logMessage(FATAL, "套接字创建失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(1);}// TCP客户端同样的不需要主动绑定自己的ip和port// 对于TCP客户端来说,需要的是连接能力,那么一个套接字足以struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof server_addr);server_addr.sin_family = AF_INET;server_addr.sin_port = htons(atoi(argv[2])); // 是要发送到网络上的,所以千万别忘了转为网络字节序if (0 > inet_aton(argv[1], &server_addr.sin_addr)) // 主机转网络 ip 点分十进制{logMessage(FATAL, "ip从本地转化网络字节序失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(2);}socklen_t sever_len = sizeof server_addr;if ( 0 > connect(clientSock, (struct sockaddr*)&server_addr, sever_len)){logMessage(FATAL, "服务器连接失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(3);}logMessage(NORMAL, "服务器连接成功~");// 客户端不断向服务器端发送信息接收信息即可std::string message;char buffer[1024];while (true){printf("请输入# ");std::getline(std::cin, message);if (message == "quit") break;// 使用send可以发送if ( 0 > send(clientSock, message.c_str(), message.size(), 0)) // 阻塞发送{logMessage(ERROR, "客户端发送失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);continue;}// 使用read接收ssize_t n = read(clientSock, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';std::cout << "server# " << buffer << std::endl;}else if (n == 0){// 此时对端关闭,我也关闭即可break;}else{logMessage(FATAL, "接收服务器数据失败!%d-%s (file:%s func:%s line:%d)", errno, strerror(errno), __FILE__, __func__, __LINE__);exit(4);}}close(clientSock);return 0;
}
上述就是TCP实现套接字编程的大致过程与思路。基本接口均已介绍完毕,这里不再演示效果。如果有后续补充我会继续在此时写,如果有错误还请大佬指出!
另外,对于TCP来说,我们在Linux环境下可以使用netstat -antp查看tcp协议的服务,l是只查看监听状态 t就是tcp,换为u就是udp了。
存在一个工具可以简单替代一下TCP的客户端:telnet 如果没有请使用sydo yum install telnet 进行安装即可。enter 运行,ctrl+] + quit可以退出。
相关文章:
【Linux】网络套接字编程
前言 在掌握一定的网络基础,我们便可以先从代码入手,利用UDP协议/TCP协议进行编写套接字程序,明白网络中服务器端与客户端之间如何进行连接并且通信的。 目录 一、了解源目的IP、端口、网络字节序、套接字 端口号: 套接字&…...
break与continue关键字
1.概述 不知道大家有没有这样一种感受哈,有的时候容易混淆break语句和continue语句的用法,总是模棱两可,不敢确定自己是否使用正确了。正好,我们本篇的重点就是break和continue关键字的用法。 2.使用场景 Java中为啥会诞生break…...
kafka使用入门案例与踩坑记录
每次用到kafka时都会出现各种奇怪的问题,综合实践,下面汇总下主要操作步骤: Docker镜像形式启动 zookeeper启动 docker run -d --name zookeeper -p 2181:2181 -t wurstmeister/zookeeperkafka启动 docker run --name kafka01 -p 9092:909…...
系统启动太慢,调优后我直呼Nice
问题背景最近在负责一个订单系统的业务研发,本来不是件困难的事。但是服务的启动时间很慢,慢的令人发指。单次启动的时间约在10多分钟左右,基本一次迭代、开发,大部分的时间都花在了启动项目上。忍无可忍的我,终于决定…...
java知识点
文章目录异常写法JVM加载反射访问private调用方法动态代理注解元数据:TargetRetention元注解泛型编写泛型擦拭法局限通配符无限定通配符(<?>)集合重写方法和实现类IO流字节与字符转换同步和异步可以设置编码的类Print*类Files时间与日期时区一种二种三种异常…...
文件的打开关闭和顺序读写
目录 一、文件的打开与关闭 (一)文件指针 (二) 文件的打开和关闭 二、文件的顺序读写 (一)fputc 1. 介绍 2. 举例 (二)fgetc 1. 介绍 2. 举例1 3. 举例2 (三&…...
(十八)操作系统-进程互斥的软件实现方法
文章目录一、知识总览二、单标志法三、双标志先检查法四、双标志后检查法五、Peterson算法六、总结一、知识总览 二、单标志法 算法思想:两个进程在访问临界区后,会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进…...
2023年三月份图形化一级打卡试题
活动时间 从2023年3月1日至3月21日,每天一道编程题。 本次打卡的规则如下: 小朋友每天利用10~15分钟做一道编程题,遇到问题就来群内讨论,我来给大家答疑。 小朋友做完题目后,截图到朋友圈打卡并把打卡的截图发到活动群…...
linux 防火墙管理-firewalld
什么是Firewalld 当前很多linux系统中都默认使用 firewalld(Dynamic Firewall Manager of Linux systems,Linux系统的动态防火墙管理器)服务作为防火墙配置管理工具。 “firewalld”是firewall daemon。它提供了一个动态管理的防火墙&#x…...
2023年最新大厂开发面试题(滴滴,华为,京东,腾讯,头条)
2023年最新大厂开发面试题!!! 滴滴篇 B树、B-树的区别? 数据库隔离级别,幻读和不可重复读的区别? 有 hell, well, hello, world 等字符串组,现在问能否拼接成 helloworld,代码实现。 快排算…...
2023年三月份图形化三级打卡试题
活动时间 从2023年3月1日至3月21日,每天一道编程题。 本次打卡的规则如下: 小朋友每天利用10~15分钟做一道编程题,遇到问题就来群内讨论,我来给大家答疑。 小朋友做完题目后,截图到朋友圈打卡并把打卡的截图发到活动群…...
蓝桥杯算法模板
模拟散列表拉链法import java.io.*; import java.util.*; public class a1 {static int n;static int N100003;static int[] hnew int[N];static int[] enew int[N];static int[] nenew int[N]; static int idx; static void insert(int x){int k(x%NN)%N;e[idx]x;ne[idx]h[k];…...
python之并发编程
一、并发编程之多进程 1.multiprocessing模块介绍 python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程。Python提供了multiprocessing。 multiprocess…...
Vue.js自定义事件的使用(实现父子之间的通信)
vue v-model修饰符:.lazy、.number、.trim $attrs数据的透传,在组件(这个是写在App.vue中),数据就透传到student组件中,在template中可以直接使用{{$attrs.students}}获取数据 通过defineProps定义的属性在attrs中就…...
第12天-商品维护(发布商品、商品管理、SPU管理)
1.发布商品流程 发布商品分为5个步骤: 基本信息规格参数销售属性SKU信息保存完成 2.发布商品-基本信息 2.1.会员等级-会员服务 2.1.1.会员服务-网关配置 在网关增加会员服务的路由配置 - id: member_routeuri: lb://gmall-memberpredicates:- Path/api/member/…...
动态分区分配计算
动态分区分配 内存连续分配管理分为: 单一连续分配固定分区分配动态分区分配(本篇所讲) 首次适应算法(First Fit,FF) 该算法又称最先适应算法,要求空闲分区按照首地址递增的顺序排列。 优点…...
【云原生】k8s的pod基本概念
一、资源限制 Pod 是 kubernetes 中最小的资源管理组件,Pod 也是最小化运行容器化应用的资源对象。一个 Pod 代表着集群中运行的一个进程。kubernetes 中其他大多数组件都是围绕着 Pod 来进行支撑和扩展 Pod 功能的,例如用于管理 Pod 运行的 StatefulSe…...
【史上最全面esp32教程】激光与食人鱼模块篇
文章目录食人鱼模块模块介绍连线说明操作激光模块模块介绍连线说明操作总结提示:以下是本篇文章正文内容,下面案例可供参考 食人鱼模块 模块介绍 采用食人鱼LED设计制作一个发光的电子模块,其实他的本质和LED无区别。 连线说明 名称接线…...
《代码整洁之道》二之有意义的命名
1.有意义的命名 1.1 名副其实 取个好名字需要花时间,但是价值远超取名的时间,一旦发现更好的名称就换掉旧的。这么做,读你代码的人都会很开心。 变量名、方法名、类名称需要清晰的告诉别人含义,如果名称需要注释来补充…...
天气预测demo
天气预测1 数据集介绍1.1 训练集1.2 测试集2 导入数据进行数据分析2.1 浏览数据2.2 探索数据2.2.1 查看数据类型1 数据集介绍 1.1 训练集 训练集中共有116369个样本,每个样本有23个特征,特征具体介绍如下: 列名解释Date:日期&a…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...
《Offer来了:Java面试核心知识点精讲》大纲
文章目录 一、《Offer来了:Java面试核心知识点精讲》的典型大纲框架Java基础并发编程JVM原理数据库与缓存分布式架构系统设计二、《Offer来了:Java面试核心知识点精讲(原理篇)》技术文章大纲核心主题:Java基础原理与面试高频考点Java虚拟机(JVM)原理Java并发编程原理Jav…...
简单介绍C++中 string与wstring
在C中,string和wstring是两种用于处理不同字符编码的字符串类型,分别基于char和wchar_t字符类型。以下是它们的详细说明和对比: 1. 基础定义 string 类型:std::string 字符类型:char(通常为8位)…...
