目录
一、预备知识
1.端口
2.TCP协议和UDP协议
3.socket编程接口
①socket 常见API
②sockaddr结构
二、网络编程
1.UDP网络程序
1.1服务器
①打印
②socket编辑
③bind
④recvfrom 编辑
1.2客户端
①sendto
1.3提升通信的花样性
①将字符串返还
②注册
③消息路由
一、预备知识
1.端口
上篇我们讲到,就是数据与数据的交互,那数据之间的交互又是用户与用户之间的交互的体现,但本质上还是用户通过进程来进行与对面用户的进程通信。
但是每个主机上的进程成千上百哪能对应准确的进程来通信?
需要两个数据就可以准确定位。
IP:标识主机的唯一性;
端口号(port):标识主机内的进程的唯一性;
IP + port = socket ->全网唯一一个进程。
一个进程可以和多个端口号绑定,但一个端口号只能和一个进程绑定。
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要 发给谁"。
2.TCP协议和UDP协议
这两个都是传输层协议。
TCP协议: UDP协议
①有链接 ①无连接
②可靠传输 ②不可靠传输
③面向字节流 ③面向数据报
3.socket编程接口
①socket 常见API
// 创建 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);
②sockaddr结构
我们看到这些接口中都有struct sockaddr*类型的参数,这个类型到底是什么?
网络通信本质是不同主机进程之间的通信,那使用网络通信的接口让同一主机的进程之间通信也是可以的。
那怎么实现呢?
靠类型的强转实现。
特定函数在使用结构之前,会先提取判断前16位数字,如果地址类型是AF_INET,就将struct sockaddr强转为struct sockadd_in类型,如果地址类型是AF_UNIX就强转为struct sockadd_un类型。
二、网络编程
1.UDP网络程序
当然你想配合整体代码看的话,请点击此处的gitee浏览。
1.1服务器
①打印
为了方便打印,我们先写一个打印函数。
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char logInfor[1024];
char *name = getenv("USER");
va_list ap;
va_start(ap, format);
vsnprintf(logInfor, sizeof(logInfor) - 1, format, ap);
va_end(ap);
FILE * out = (level == FATAL) ? stderr : stdout;
fprintf(out,"%s | %u | %s | %s\n",\
log_level[level],\
(unsigned int)time(nullptr),\
name == nullptr ? "Unkown" : name,\
logInfor
);
}
②socket
目的是创建一个套接字。
domain:本地通信还是网络通信
一般有这几个选项:
type:套接字类型决定了通信的时候的报文类型,一般有流式、用户数据报类型。
一般有这几个选项:
protocol:协议类型。网络通信中设置为0。
返回值:成功一个文件描述符被返回,失败返回-1,errno被设置。
首先来使用一下socket这个函数。
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
{
logMessage(FATAL, "%s%d", strerror(errno), fd);
exit;
}
logMessage(DEBUG, "socket create success : %d", fd);
return 0;
}
结果:
还要建一个类,作为初始的框架
class UdpServer
{
public:
UdpServer()
{
}
~UdpServer()
{
}
public:
void init()
{
}
void start()
{
}
private:
int sockfd;
};
这里的创建套接字要放到init初始化中。
// 1.create 套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
logMessage(FATAL, "socket: %s%d", strerror(errno), _sockfd);
exit(1);
}
logMessage(DEBUG, "socket create success : %d", _sockfd);
③bind
给一个套接字绑定上iP地址与端口号。
// 2 bind
// 2.1 填入基本信息到struct sockaddr_in 中
struct sockaddr_in local;
// 初始化
bzero(&local, sizeof(local));
// 填充域 AF_INET 网络通信 AF_UNIX 本地通信
local.sin_family = AF_INET;
// 填充对应的端口号 htons的作用是将本地序列转换为网络序列这样才能发送给对方
local.sin_port = htons(_port);
// 服务器的IP地址 xx.yy.aa.ccc 每个都是0-255的数字,有四个8比特位 正好放在uint36_t中
// sin_addr也是一个结构体其中的元素是s._addr是被typedef过的uint32_t
// INADDR_ANY就是0,一般不关注服务器绑定哪一个IP地址,服务器会自动bind,一般所有服务器都是这样做的
// inet_addr将char *转换为s_addr,还会将主机序列转换为网络序列
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
// 2.2 bind网络信息
if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) == -1)
{
logMessage(FATAL, "bind: %s%d", strerror(errno), _sockfd);
exit(2);
}
logMessage(DEBUG, "socket success: %d", _sockfd);
其中sockaddr_in的成员为:
其中包含端口号和IP地址。
这时大致udpserver已经成型,我们来测试下。
static void Usage(const string proc)
{
cout << "Usage:\n\t" << proc << " port [ip]" << endl;
}
void start()
{
while (1)
{
logMessage(NOTICE, "udpserver runing");
sleep(1);
}
}
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
string ip;
if (argc == 3)
{
ip = argv[2];
}
UdpServer svr(port, ip);
svr.init();
svr.start();
return 0;
}
这里的端口号最好不要绑定0-1023的端口号,这些端口号是服务器自己使用的对应特定服务的。 这时我们可以通过netstat -lnup来查看当前的网络服务。
④recvfrom
从特定socke中读取到buf里,长度为len,默认设置flags为0,阻塞式读取,src_addr用来接收发送方的参数,addrlen为src_addr的大小。返回值为读到的字节大小。
void start()
{
char inbuffer[1024]; // 输入进来的数据放到inbuffer中
char outbuffer[1024]; // 输出的数据放outbuffer中
while (1)
{
struct sockaddr_in peer; // 输出形参数
socklen_t len = sizeof(peer); // 输入输出型参数
ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (size > 0)
{
// 这里将读的数据看为字符串
inbuffer[size] = 0;
}
else if (size == -1)
{
logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
}
// 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
// peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
string peerip = inet_ntoa(peer.sin_addr);
uint16_t peerport = ntohs(peer.sin_port);
// 打印客户端IP与port 和信息
logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
}
}
1.2客户端
以上写的都是服务端,现在完善一下客户端。
上面提到过作为服务器不用bind特定的IP地址和port端口号,但是作为客户端必须知道服务器的IP地址和port端口号。
static void Usage(const string proc)
{
cout << "Usage:\n\t"
<< "server IP ,server port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 获取服务端
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 2. 创建客户端
// 2.1 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
// 2.3 填写对应服务器信息
struct sockaddr_in server;
bzero(&server, sizeof(server));
// 都需要转换为网络序列
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
...
①sendto
既然到了客户端,服务端来接收,客户端就要发送。
通过指定的套接字,从缓冲区buf中读取len的长度的内容,默认flags为0阻塞式,dest_addr为目的地。
// 3. 发送消息
string output;
while (1)
{
cout << "Please entry | ";
getline(cin, output);
// 发送
sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
}
写了这么多,我们来测试下。
这里的IP地址127.0.0.1,是本地环回,就指的是本主机。
当然这是本地之间的测试,你如果想远程测试可以,将udpclient发给他,然后将自己的IP地址告诉对方,自己打开udpserver并确定端口号,对方使用udpclient通过IP地址和端口号就可以远程通信了。当然记得使用g++进行编译时要加-static,库变成静态连接,这样对面没有对应的库也没有关系。具体的可以看我这篇博客。
那要是没有两台linux呢?我们可以用Visual Studio做出windows版本的客户端。
代码:
#pragma warning(disable:4996)// 使warning去掉
#pragma comment(lib,"ws2_32.lib")// 所需要包含的连接库
#include <iostream>
#include <cstdio>
#include <cassert>
#include <string>
#include <WinSock2.h>
using namespace std;
int serverport = 8888;
string serverip ="121.4.139.131"; // 此处要填自己主机的IP地址
int main()
{
// 用作初始化套接字
WSADATA data;
// 初始套接字
WSAStartup(MAKEWORD(2, 2), &data);
(void)data;
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
struct sockaddr_in server;
memset(&server, 0 ,sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
string output;
while (1)
{
cout << "Please entry | ";
getline(cin, output);
sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr*)&server, sizeof(server));
}
closesocket(sockfd);
WSACleanup();
return 0;
}
结果:
注意:有些云服务器没有开放对应的端口,是不能使用这种Udp方法通信的。我们要在自己云服务器的官网找到你的云服务器,在你的实例中点击防火墙,然后点击添加规则,我们这次测试为Udp所以点击Udp,再将你自定义的端口输入,点击确定,这样你就可以和远处的人通信了。虽然这次实验只是单方面的通信。
1.3提升通信的花样性
①将字符串返还
我们服务端只接受信息太单一了,我们将发送过来的字符串,转换成大写转换回去。这些代码分别写在上一步server打印信息和client发送信息之后。
代码:
UdpServer.cc:start():
// 转换字符串小写-大写
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, (struct sockaddr *)&peer, len);
memset(outbuffer,0,sizeof(outbuffer));
UdpClient.cc:main():
// 接收
char buffer[1024];
struct sockaddr temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
if (s > 0)
{
buffer[s] = 0;
cout << "Server output| " << buffer << endl;
}
结果:
②注册
使用unordered_map来简单存储用户信息,来区分新老用户。
客户端没有任何变化,服务端新增一个成员变量,将转换字符注释掉,向客户端发送字符也注释掉。注意服务端的代码都是写在类中的。
代码:
void start()
{
char inbuffer[1024]; // 输入进来的数据放到inbuffer中
char outbuffer[1024]; // 输出的数据放outbuffer中
int i = 1;
while (1)
{
struct sockaddr_in peer; // 输出形参数
socklen_t len = sizeof(peer); // 输入输出型参数
ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (size > 0)
{
// 这里将读的数据看为字符串
inbuffer[size] = 0;
}
else if (size == -1)
{
logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
}
// 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
// peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
string peerip = inet_ntoa(peer.sin_addr);
uint16_t peerport = ntohs(peer.sin_port);
// 打印客户端IP与port 和信息
logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
checkOnlineUser(i,peerip, peerport, peer);
}
}
bool checkOnlineUser(int &i,string &ip, uint16_t port, struct sockaddr_in &peer)
{
string userInfor = ip;
userInfor += " ";
userInfor += to_string(port);
auto iter = user.find(userInfor);
if (iter == user.end())
{
// 没找到
user.insert({userInfor, peer});
i = 1;
if (i == 1)
{
cout << "新用户登录" << endl;
}
}
else
{
// 找到了
if (i == 1)
{
i = 0;
cout << "老用户登录" << endl;
}
}
}
private:
unordered_map<string, struct sockaddr_in> user;
③消息路由
将输出与命名管道结合起来。
先在当前路径建立命名管道,修改代码,服务端中将客户端发回的消息使用管道保存起来。代码:
void start()
{
......
logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
checkOnlineUser(i, peerip, peerport, peer);
messageRoute(inbuffer);
}
void messageRoute(string message)
{
for (auto &ch : user)
{
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(ch.second),sizeof(ch.second));
}
}
结果:
我们呢,还可以使用线程来改造下。让主线程不断在发消息,另一个线程去接收发回来的消息。
代码:
struct sockaddr_in server;
static void Usage(const string proc)
{
cout << "Usage:\n\t"
<< "server IP ,server port" << endl;
}
void *fuc(void *argc)
{
while (1)
{
int sockfd = *(int *)argc;
// 接收
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
struct sockaddr temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
if (s > 0)
{
buffer[s] = 0;
cout << "Server output| " << buffer << endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1. 获取服务端
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 2. 创建客户端
// 2.1 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
// 2.3 填写对应信息
bzero(&server, sizeof(server));
// 都需要转换为网络序列
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
pthread_t t;
pthread_create(&t, nullptr, fuc, (void *)&sockfd);
// 3. 发送消息
string output;
while (1)
{
cerr << "Please entry | ";
getline(cin, output);
// 发送
sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
}
close(sockfd);
return 0;
}
运行过程与上文的结果一样,这里就不演示了。
这里的fifo好像没什么用啊?为什么要管道呢?
我们启动服务器,在客户端输入消息,服务器再将消息返回,使用fifo来展示返回的消息。一个人这样使用其实没有fifo用处,但如果是多个人使用,无论谁向客户端输入消息,服务器都会返还所有它接受的消息到fifo中,意味着我们可以通过fifo来查看别人发的消息,这样不就可以双端聊天了嘛。至于fifo中不显示谁发送的消息,我们将server中的sendto所使用的buffer中填写从recevfrom中获取到的IP地址与端口,就可以辨识是谁发送的消息。
到这里我们Udp网络程序的编写暂时告一段落基本的要求我们都已经实现,Udp的全部代码我已上传gitee,有兴趣的可以看一下。
限于篇幅,Tcp网络编程就移至下一节去讲,感谢观看,如有错误请指出,我们下次再见。