套接字Socket
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
总结:套接字相当于我们之间通信的桥梁,我们接收与发送的所有信息都要通过套接字。
数据的传输协议
数据传输方式,不管是有线传输还是无线传输,数据传输的方式有两种:
1、TCP(Transfer Control Protocol)传输控制协议方式,该传输方式是一种稳定可靠的传送方式,类似于现实中的打电话。只需要建立一次连接,就可以多次传输数据。
2、UDP(User Datagram Protocol)用户数据报协议方式,该传输方式不建立稳定的连接,类似于发短信息。每次发送数据都直接发送,发送多条短信,就需要多次输入对方的号码。该传输方式不可靠,数据有可能收不到,系统只保证尽力发送。使用该种方式的优点是开销小,传输速度快,缺点是数据有可能会丢失。
总结:TCP比UDP稳定但TCP成本高, UDP无连接, TCP有链接
网络字节序
,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1.网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
2.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
3.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
4.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket编程接口
socket 常见通用接口
首先网络通信的标志有很多种,但如果每一种通信都设计一个函数太麻烦,因此有一个统一的接口函数,我们在使用的时候根据自己的通信更改一下参数就可以了,这样就方便了很多。
如下图,我们统一接口为sockaddr,当用sockaddr_in时只需要做一个类型转换就可以。
1.创建套接字
2.绑定端口和ip
因为下面我们要用到的套接字种类为sockaddr_in,我简单介绍下这个结构体里面都包含了哪些信息
需要包含头文件#include <netinet/in.h>与#include <arpa/inet.h>与上面的#include <sys/types.h>#include <sys/socket.h>这四个是在网络编程中经常用到的头文件
3.接收信息函数
4.发送信息函数
简单的UDP网络程序,模拟UDP协议实现能接收客户端发送消息的服务端(注释很重要)
1.udp_server.cpp
#include <iostream>
#include <string.h>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage()
{
cout << "输入格式有误"
<< "应为:./udp_server + 端口号" << endl;
}
//输入格式./udp_server + 端口号
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage();
return 1;
}
uint16_t port = atoi(argv[1]);
// 1.创建套接字打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
cerr << "socket create error" << errno << endl;
return 2;
}
cout << sock << endl;
//作为服务器必须要让客户知道对应的服务器地址(ip + port)
// 2.给服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //地址家族
local.sin_port = htons(port); //此处的端口号,是我们计算机上的变量,是主机序列, 因为在栈上创建的,我们要转为网络字节序需要利用htons函数前面有介绍
// a. 需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP
// b. 也要考虑大小端
// in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作.
// 坑:
// 云服务器,不允许用户直接bind公网IP,另外, 实际正常编写的时候,我们也不会指明IP
// local.sin_addr.s_addr = inet_addr("42.192.83.143"); //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
// INADDR_ANY: 如果你bind的是确定的IP(主机), 意味着只有发到该IP主机上面的数据
// 才会交给你的网络进程, 但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是
// 某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_addr.s_addr = INADDR_ANY;
//开始绑定
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error" << errno << endl;
return 3;
}
// 3.提供服务,注意网络服务24小时不间断,因此始终是死循环
bool quit = false;
while (!quit)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
//我要提供一段空间来接收客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (cnt > 0)
{
//因为我们要接受的是字符串,所以我们加\0, 否则不需要加,比如发过来的是命令就不需要加
buffer[cnt] = 0;
cout << "读到客户端发来的信息:" << buffer << endl;
//根据用户输入。构建一个新的返回字符串
string str = buffer;
str += " 老哥我收到了!";
//发送给的人正是我刚才提供一段空间来接收客户端信息的peer变量, 里面一定有客户端的ip+port
sendto(sock, str.c_str(), str.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
// TODO
}
}
return 0;
}
2.udp_client.cpp
#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
void Usage()
{
cout << "输入格式有误"
<< "应为:./udp_client + ip + 端口号" << endl;
}
//输入格式为 ./udp_client + ip + 端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 0;
}
// 1.创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
cerr << "sock create fail" << errno << endl;
return 1;
}
//客户端需要显示的bind的吗??
// a. 首先,客户端必须也要有ip和port
// b. 但是,客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联
// client指明的端口号,在client端一定会有吗??有可能被占用,被占用导致client无法使用
// server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind()
// 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
// 2.使用服务
// a.我要发给谁?我们一开始输入的ip + port就是我们要发送的主机信息
//我们要发给服务端,因此我们要创建结构存放服务端的地址信息
struct sockaddr_in server;
server.sin_family = AF_INET; //地址家族
server.sin_port = htons(atoi(argv[2])); //这里是主机序列,要发送到网络要转为网络字节序,因此使用htons
server.sin_addr.s_addr = inet_addr(argv[1]); //同样也要转网络字节序利用 inet_addr
while (1)
{
// b.我的数据从哪里来
string message;
cout << "请输入发送的数据:";
cin >> message;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
//因为要接受服务端的信息,需要sockaddr的参数,但实际上我们用不上这个参数,因为我们已经知道服务端的地址信息,
//因此,接收函数的sockaddr参数tmp只是一个占位符
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
size_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len);
if(s > 0)
{
buffer[s] = 0;
cout << "服务端反馈的信息为:" << buffer << endl;
}
}
return 0;
}
3.Makefile
.PHONY:all
all:udp_server udp_client
udp_server:udp_server.cpp
g++ -o $@ $^ -std=c++11
udp_client:udp_client.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server