赞
踩
目录
上篇我们讲到,就是数据与数据的交互,那数据之间的交互又是用户与用户之间的交互的体现,但本质上还是用户通过进程来进行与对面用户的进程通信。
但是每个主机上的进程成千上百哪能对应准确的进程来通信?
需要两个数据就可以准确定位。
IP:标识主机的唯一性;
端口号(port):标识主机内的进程的唯一性;
IP + port = socket ->全网唯一一个进程。
一个进程可以和多个端口号绑定,但一个端口号只能和一个进程绑定。
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要 发给谁"。
这两个都是传输层协议。
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*类型的参数,这个类型到底是什么?
网络通信本质是不同主机进程之间的通信,那使用网络通信的接口让同一主机的进程之间通信也是可以的。
那怎么实现呢?
靠类型的强转实现。
特定函数在使用结构之前,会先提取判断前16位数字,如果地址类型是AF_INET,就将struct sockaddr强转为struct sockadd_in类型,如果地址类型是AF_UNIX就强转为struct sockadd_un类型。
当然你想配合整体代码看的话,请点击此处的gitee浏览。
为了方便打印,我们先写一个打印函数。
- #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
- );
- }
目的是创建一个套接字。
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);
给一个套接字绑定上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来查看当前的网络服务。
从特定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);
- }
- }
以上写的都是服务端,现在完善一下客户端。
上面提到过作为服务器不用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);
- ...
既然到了客户端,服务端来接收,客户端就要发送。
通过指定的套接字,从缓冲区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,再将你自定义的端口输入,点击确定,这样你就可以和远处的人通信了。虽然这次实验只是单方面的通信。
我们服务端只接受信息太单一了,我们将发送过来的字符串,转换成大写转换回去。这些代码分别写在上一步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网络编程就移至下一节去讲,感谢观看,如有错误请指出,我们下次再见。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。