初识网络及socket编程基础
Posted WoLannnnn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识网络及socket编程基础相关的知识,希望对你有一定的参考价值。
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
源ip地址就是发送端ip,目的ip地址就是接收端ip
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出这个数据要给哪个程序进行解析,于是我们引出了端口号
认识端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
因此,套接字通信本质是进程间通信
理解 “端口号” 和 “进程ID”
pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
答:进程id的作用是在系统的多个进程中标识某一个进程,而端口号则是在众多网络进程中标识某一个进程
比如10086,10086相当于ip地址,而我们转接的人工客服相当于一个服务器进程,员工编号是端口号,客服的身份证号是pid
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号.
源端口号就是发送数据的主机进程端口号,目的端口号就是接收数据的主机进程端口号。即描述 “数据是谁发的, 要发给谁”;
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
TCP会处理丢包了的情况,也就是会保证一定的效率
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
与TCP不同,UDP只负责发送,丢包了它并不会处理,也就是不会保证发送数据的效率
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //把uint32_t类型从主机序转换到网络序
uint16_t htons(uint16_t hostshort); //把uint16_t类型从主机序转换到网络序
uint32_t ntohl(uint32_t netlong); //把uint32_t类型从网络序转换到主机序
uint16_t ntohs(uint16_t netshort); //把uint16_t类型从网络序转换到主机序
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket编程接口
socket 常见API
// 创建 socket文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:domain表示域,表明使用的某种协议;type是套接字类别;protocol表示所采用协议,一般设置为0,也就是系统默认的
domain:我们常用的是AF_INET选项;type:我们主要用的两种,一种是TCP协议的sock_STREAM,一种是UDP协议的sock_DGRAM
返回值:创建成功返回文件描述符,失败返回-1.由此看出套接字也是文件
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
参数:socket表示需要绑定的套接字文件描述符,address是sockaddr的地址,address_len表示address指向的结构体的大小
// 开始监听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结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
观察上面三种结构体,第一个成员变量都是一个16位地址,是为了方便引用第一个变量,从而判断它属于哪种结构体。而socket的API一般都是struct sockaddr*,在底层的实现又各不相同,这种原理实际上就是多态,通过一套接口完成多种通信。
Linux下一切皆文件也是利用了多态。
sockaddr 结构
struct sockaddr
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
;
sockaddr_in 结构
struct sockaddr_in
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
;
in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
- n_family:指代协议族,在socket编程中只能是AF_INET
- sin_port:存储端口号(使用网络字节顺序)
- sin_addr:存储IP地址,使用in_addr这个数据结构,也就是上面图片中的
- sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
虽然socket API的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主
要有三部分信息: 地址类型, 端口号, IP地址
简单的UDP网络程序
实现一个简单的英译汉的功能
备注: 代码中会用到 地址转换函数 .
UDP通用服务器和客户端
里面要用到的几个接口:
//1.
ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
int flags, struct sockaddr *restrict address,
socklen_t *restrict address_len);
参数:socket:套接字文件描述符;restrict buffer:接收数据的缓冲区(内存块);length:预计接收的字节长度;flags:表示没有数据时是否阻塞; restrict address:输出型参数,指向(接收)发送者的信息(ip、port等);
restrict address_len:输入输出型参数,输入的是参数restrict address的长度,输出的是实际返回给restrict address的结构体的大小
返回值:实际接收到的数据的大小
//2.
ssize_t sendto(int socket, const void *message, size_t length,
int flags, const struct sockaddr *dest_addr,
socklen_t dest_len);
与recvfrom函数的参数类似,message表示保存发送信息的内存块,dest_addr保存接受者的信息,dest_len是dest_addr结构体的大小
//3.
in_addr_t inet_addr(const char *cp);
将字符串ip转换成in_addr_t,
什么是in_addr_t:
typedef uint32_t in_addr_t;//32位的无符号整型
//4.
in_addr转字符串的函数:
char *inet_ntoa(struct in_addr inaddr);
服务器端
udpServer.hpp:
#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class udpServer
private:
string _ip;//ip地址
int _port;//端口号
int _sock;//套接字文件描述符
public:
//构造
udpServer(string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
//初始化服务器
void initServer()
//1.创建套接字
//2.将套接字与ip、端口号绑定
_sock = socket(AF_INET, SOCK_DGRAM, 0);
//套接字也是文件,所以_sock实际上也是一个文件描述符,或者说,它就是3
cout << _sock << endl;
sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//将主机端口转换成网络端口
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//因为bind的第二个参数是struct sockaddr*类型的,所以需要强转
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
cerr << "bind error!\\n" << endl;
exit(1);
// 启动服务器
void start()
char msg[64];
while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态
msg[0] = '\\0';
struct sockaddr_in end_point;//接收远端客户端的信息:ip、port等
socklen_t len;//客户端信息结构体的的大小
ssize_t s = recvfrom(_sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);//len是一个输入输出型参数
if (s > 0)
msg[s] = '\\0';
//打印接收到的数据
cout << "client say# " << msg << endl;
//服务器回应
string echo_string(msg, msg + s);
//string echo_string;
echo_string += "[server receive]";
//发送信息
sendto(_sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);
//析构,没什么资源需要清理
~udpServer()
;
udpServer.cpp
#include"udpServer.hpp"
int main()
auto pus = new udpServer;
pus->initServer();
pus->start();
delete pus;
return 0;
客户端
udpClient.hpp
#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class udpClient
private:
string _ip;//ip地址
int _port;//端口号
int _sock;//套接字文件描述符
public:
//构造
//客户端的_ip和_port是服务器的,因为他要发信息给服务器
udpClient(string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
//初始化客户端
void initClient()
//客户端不需要绑定
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
// 启动客户端
void start()
//先发再收
string msg;
struct sockaddr_in peer;//保存接收者的信息:ip、port等
peer.sin_family = AF_INET;
peer.sin_port = htons(_port);//将主机端口转换成网路端口
peer.sin_addr.s_addr = inet_addr(_ip.c_str());
socklen_t len = sizeof(peer);//接收端信息结构体的的大小
while (1)
cout << "Please Enter:";
cin >> msg;
//发送信息
sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, len);
//接收信息
char echo[128];
ssize_t s = recvfrom(_sock, echo, sizeof(echo) - 1, 0, nullptr, nullptr);//不关心发送者的信息
if (s > 0)
echo[s] = '\\0';
//打印接收到的数据
cout << echo << endl;
//析构,没什么资源需要清理
~udpClient()
;
udpClient.cpp
#include"udpClient.hpp"
int main()
auto uct = new udpClient;
uct->initClient();
uct->start();
delete uct;
return 0;
效果:
为什么服务器需要绑定端口号,而客户端无须绑定?
服务器是要一直运行的,除非更新等情况才会停止运行,所以要固定一个ip地址和端口号,以便客户端访问,而客户端不用一直运行,所以使用系统分配的合适一点,因为系统清楚端口号的分配。如果使用自己定义的,可能这个ip或端口号正在被使用或者正要被使用,这样就会导致绑定失败,从而客户端运行不起来。
服务器与客户端对话2.0
主要改动是以命令行参数的形式传ip、端口号;server端不用固定ip,这样每一个客户端只要端口一致就可以和它通信;展示客户端信息(ip、port)
udpServer.hpp(ip地址设置成INADDR_ANY,展示客户端信息)
#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class udpServer
private:
//string _ip;//ip地址
int _port;//端口号
int _sock;//套接字文件描述符
public:
//构造
udpServer(int port = 8080)
:_port(port)
//初始化服务器
void initServer()
//1.创建套接字
//2.将套接字与ip、端口号绑定
_sock = socket(AF_INET, SOCK_DGRAM, 0);
//套接字也是文件,所以_sock实际上也是一个文件描述符,或者说,它就是3
cout << _sock << endl;
sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//将主机端口转换成网络端口
local.sin_addr.s_addr = INADDR_ANY;//任意绑定
//因为bind的第二个参数是struct sockaddr*类型的,所以需要强转
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
cerr << "bind error!\\n" << endl;
exit(1);
// 启动服务器
void start()
char msg[64];
while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态
msg[0] = '\\0';
struct sockaddr_in end_point;//接收远端客户端的信息:ip、port等
socklen_t len;//客户端信息结构体的的大小
ssize_t s = recvfrom(_sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);//len是一个输入输出型参数
if (s > 0)
//获取客户端的ip和port
char buf[16];
//端口号,整型转成点分十进制
sprintf(buf, "%d", ntohs(end_point.sin_port));
string cli = inet_ntoa(end_point.sin_addr);//ip地址
cli+=":";
cli+=buf;
msg[s] = '\\0';
//打印接收到的数据
cout << cli << "# " << msg << endl;
//服务器回应
string echo_string(msg, msg + s);
//string echo_string;
echo_string += "[server receive]";
//发送信息
sendto(_sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);
//析构,没什么资源需要清理
~udpServer()
close(_sock);
;
为什么ip地址是127.0.0.1?
127.0.0.1代表本地环回,通常用来进行网络通信代码的本地测试。一般跑通了,就说明本地环境以及代码基本没有大问题。
服务器的ip赋值成INADDR_ANY:任意绑定,服务器收到任意ip的信息都可以接收,不会只限定一个ip地址。
udpClient.hpp(不变)
#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class udpClient
private:
string _ip;//ip地址
int _port;//端口号
int _sock;//套接字文件描述符
public:
//构造
//客户端的_ip和_port是服务器的,因为他要发信息给服务器
udpClient(string ip = "127.0.0.1", int port = 8080)
:_ip(ip)
,_port(port)
//初始化客户端
void initClient()
//客户端不需要绑定
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
// 启动客户端
void start()
//先发再收
string msg;
struct sockaddr_in peer;//保存接收者的信息:ip、port等
peer.sin_family = AF_INET;
peer.sin_port = htons(_port);//将主机端口转换成网路端口
peer.sin_addr.s_addr = inet_addr(_ip.c_str());
socklen_t len = sizeof(peer);//接收端信息结构体的的大小
while (1)
cout << "Please Enter:";
cin >> msg;
//发送信息
sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, len);
//接收信息
char echo[128];
ssize_t s = recvfrom(_sock, echo, sizeof(echo) - 1, 0, nullptr, nullptr);//不关心发送者的信息
if (s > 0)
echo[s] = 1初识socket