1. 理解源IP地址和目的IP地址
在IP数据包的报头中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
通过这个,我们可以定位发送主机和接收主机,但是这里存在一个问题,将数据传给应用层,实际上是把它当作一个文件,给应用层对应的进程,而准确找到传给哪一个进程,需要用到端口号
2. 认识端口号
端口号是传输层协议的内容端口号是一个2字节16位的整数端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理,所以查找进程是通过端口号IP地址 + 端口号能够标识网络上的某一台主机的某一个进程多个端口号可以指向一个进程,但是一个端口号不能指向多个进程
注意:
服务端的端口号是公众的,会被精心设计让被用户端知道
3. 理解 "端口号" 和 "进程ID"
进程 pid 表示唯一一个进程,而端口号也是唯一表示一个进程,两者的关联是:
不是所有的进程都要网络通信,但是所有的进程都要有 pid有了这两种概念,可以使得系统和网络功能解耦(耦合度降低)
4. 认识TCP协议
TCP协议的特点:
传输层协议 (传输层有一个类似哈希表,对应数组下标(端口号)和进程PCB的关联,通过端口号,找到进程结构体对象)有连接可靠传输面向字节流5. 认识UDP协议
UDP协议的特点:
传输层协议无连接不可靠传输面向数据报
6. 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分, 网络数据流同样有大端小端之分
定义网络数据流的地址 :
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据(默认低地址存的就是高位字节)如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
1. 这些函数名很好记 , 如:h 表示 host 主机 , n 表示 network 网络 , l 表示32位长整数, s 表示16位短整数
(例如 htonl 表示将32位的长整数从主机字节序转换为网络字节序)
2. 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ; 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
7. socket编程接口
(一)sockaddr 结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
套接字编程的种类:
域间套接字编程 ----- 在同一个主机内原始套接字变成 ----- 用于网络工具网络套接字编程 ----- 用于用户间网络通讯
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
(二)sockaddr_in 结构
in_addr 结构 :
头文件:
#include <netinet/in.h>
#include <arpa/inet.h>
(三)socket 常见接口
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
原型:
int socket(int domain, int type, int protocol);
参数:
domain :地址类型,一般设置成 AF_INET ( IP 地址是4字节 )
type :SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET / SOCK_RAW / SOCK_RDM / SOCK_PACKET (UDP 设置成 SOCK_DGRAM,TCP 设置成 SOCK_STREAM)
protocol :一般设置成 0
返回值:
返回文件描述符fd
绑定端口号 (TCP/UDP, 服务器)
原型:
int bind (int socket, const struct sockaddr *address, socklen_t address_len);
参数:
socket : 文件描述fd
address : 一般网络通讯,传 sockaddr_in* 类型对象,要强转成 struct sockaddr *
address_len : struct sockaddr对象的内存大小
返回值:
返回值为 -1,返回失败
接收请求(UDP)
原型:
ssize_t recvfrom ( int sockfd , void *buf , size_t len , int flags , struct sockaddr * src_addr , socklen_t *addrlen );
参数:
sockfd :文件描述符fd
buf :从文件描述符是fd的文件中读取数据
len :从文件描述符是fd的文件中读取 len个数据
flags :一般设置成 0
src_addr :接收 sockaddr *类型对象(输出性参数)
addrlen:接收 socklen_t *类型对象(输出型参数)
返回值:
buf 接收到的有效内存大小
发送信息(UDP)
原型:
ssize_t sendto (int sockfd , const void *buf , size_t len , int flags , const struct sockaddr *dest_addr , socklen_t addrlen);
参数:
sockfd : 文件描述符fd
buf : 需要传输的数据
len :需要传输的数据的大小
dest_addr : struct sockaddr *对象
addrlen : socklen_t 对象
开始监听socket (TCP, 服务器)
原型:
int listen(int socket, int backlog);
参数:
scoket : 服务端的文件描述符
backlog : 一般设置成 0
返回值:
调用成功返回 0 ,失败返回 -1
接收请求 (TCP, 服务器)
原型:
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
参数:
socket:服务端的文件描述符
address:输出型参数
address_len:输出型参数
返回值:
调用成功,返回用户端的文件描述符,失败返回 -1
建立连接 (TCP, 客户端)
原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:用户端的文件描述符
addr :输入型参数
addrlen:输出入型参数
8. 地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在用字符串表示和 in_addr表示之间转换
字符串转 in_addr的函数 :
inet_aton 和 inet_addr 的区别在于:
inet_addr开辟的 in_addr*这块空间是静态的
inet_aton 传参 in_addr*对象,是要自己去创建的
in_addr转字符串的函数:
两个函数的区别在于:
inet_ntoa 的char*开辟的空间是静态的
而inet_ntop 参数 char*对象是要自己开辟
注意:
这种静态开辟的空间实际上是不安全的,因为是公共资源,容易造成多线程进行访问
9. UDP通信
netstat -naup列出所有的UDP端口以及相关的进程信息
代码(linux之间通信)
#include"server.h"void usege(char* s){ cout << "import : " << s << " + port" << endl;}int main(int argc,char* argv[]){ if(argc != 2) { usege(argv[0]); exit(3); } uint16_t port = stoi(argv[1]); unique_ptr<udp_server> server(new udp_server(port)); server->run(); return 0;}
#pragma once#include<iostream>#include<string>#include<memory>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include<string.h>#include<unistd.h>#define SIZE 1024using namespace std;class udp_server{public: udp_server(const uint16_t port = 8080,const string& ip = "0.0.0.0") :_port(port) ,_ip(ip) {} void run() { _scoket = socket(AF_INET,SOCK_DGRAM,0); //构建socket if(_scoket < 0) { cout << "server : create fail" << endl; exit(1); } //绑定 struct sockaddr_in _addr; bzero(&_addr,sizeof(_addr)); _addr.sin_family = AF_INET; _addr.sin_port = htons(_port); _addr.sin_addr.s_addr = inet_addr(_ip.c_str()); //_addr.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(_scoket,(struct sockaddr*)&_addr,sizeof(_addr)); if(ret < 0) { cout << "server : bind fail" << endl; exit(2); } struct sockaddr_in _addret; socklen_t len = sizeof(_addret); while(true) { //接收客户端的信息 char buff[SIZE]; cout << "recvfrom begin" << endl; ssize_t n = recvfrom(_scoket,buff,sizeof(buff),0,(struct sockaddr*)&_addret,&len); if(n < 0) { cout << "recvfrom error" << endl; } buff[n] = 0; cout << buff << endl; string s = "server say : "; s += buff; //向客户端回复信息 sendto(_scoket,s.c_str(),s.size(),0,(struct sockaddr*)&_addret,len); } }private: int _scoket; //文件描述符 uint16_t _port; //端口号 string _ip; //ip地址};
#include<iostream>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>using namespace std;void useage(char *s){ cout << "输入三个参数 >> " << s << " + 服务端的端口号 + 服务端的id地址" << endl;}int main(int argc,char* argv[]){ if(argc != 3) { useage(argv[0]); exit(3); } int fd = socket(AF_INET,SOCK_DGRAM,0); if(fd < 0) { cout << "客户端创建失败" << endl; } uint16_t port = stoi(argv[1]); string s = argv[2]; //客户端不在意它自己的端口号 sockaddr_in _addr; _addr.sin_family = AF_INET; _addr.sin_port = htons(port); _addr.sin_addr.s_addr = inet_addr(s.c_str()); socklen_t len = sizeof(_addr); string arr; while(true) { char buff[1024]; sockaddr_in server; socklen_t llen; cout << "send datas:" << endl; getline(cin,arr); int ret = sendto(fd, arr.c_str(),arr.size(),0,(struct sockaddr *)&_addr,len); if(ret == -1) { cout << "sendto error" << endl; } ssize_t n = recvfrom(fd,buff,sizeof(buff),0,(struct sockaddr*)&server,&llen); cout << buff << endl; }}
注意:
服务端绑定的ip不能是公网ip(包括linux主机上的公网ip,如果绑定的地址是 "0.0.0.0",意思是,如果一个主机有多个ip地址,可以从多个ip地址发给这个主机,而不是定死了是某一个ip地址。但是服务器可以绑定"127.0.0.1"这个公网ip :本地循环地址,在一个主机上发送和传输数据,通常用来进行服务端和用户端通信的测试)客户端可以不用绑定ip,因为客户端不在意自己的端口号(系统随机给),它更关系服务端的ip地址和端口号,在第一次成功发送数据给客户端后,服务端就知道它的端口号和ip地址了注意:
在客户端首次发送数据的时候,系统会给客户端绑定端口号udp 的 socket 是全双工的,允许同时被读写
代码(windows:客户端,linux:服务端)
#include<iostream>#include<stdlib.h>#include<string>#include<stdio.h>#include <WinSock2.h>#include<Windows.h>#include <sys/types.h>#pragma comment(lib,"ws2_32.lib") #pragma warning(disable:4996)using namespace std;int main(){ //初始化SOCKET WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD(1, 1); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { return -1; } if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) { WSACleanup(); return -1; } //创建套接字int client_fd = socket(AF_INET,SOCK_DGRAM,0); if (client_fd < 0) { cout << "clinet socket fail" << endl; } uint16_t port = 8080; string ip = "1.94.49.66"; struct sockaddr_in client; int len = sizeof(client); client.sin_family = AF_INET; client.sin_port = htons(port); client.sin_addr.S_un.S_addr = inet_addr(ip.c_str()); while (true) { string s; cout << "sent datas:"; getline(cin, s); //发送信息 sendto(client_fd, s.c_str(), s.size(), 0, (struct sockaddr*)&client, len); //接收信息 char buff[1024]; struct sockaddr_in server; int llen = sizeof(server); int n = recvfrom(client_fd, buff, sizeof(buff), 0, (struct sockaddr*)&server, &llen); if (n < 0) { cout << "clinet recvfrom fail" << endl; continue; } buff[n] = 0; cout << buff << endl; } return 0;}
//服务端上述已经写过了
10. TCP通信
netstat -nltp列出所有正在监听的TCP端口以及相关的进程信息
代码 (简单版)
#include"server.h"int main(){ unique_ptr<server> svr(new server()); svr->start(); svr->run();}
#include<iostream>#include <sys/types.h> #include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include<unistd.h>#include<memory>using namespace std;class server{public: server(const uint16_t port = 8080,const string& ip = "0.0.0.0") :server_port(port) ,server_ip(ip) {} void start() { //构建套接字 server_fd = socket(AF_INET,SOCK_STREAM,0); if(server_fd < 0) { cout << "server scoket fail" << endl; exit(1); } //绑定端口号 sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(server_ip.c_str()); socklen_t len = sizeof(server); int tmp = bind(server_fd,(struct sockaddr*)&server,len); if(tmp < 0) { cout << "server bind fail" << endl; exit(2); } //开始监听 int ret = listen(server_fd,0); if(ret < 0) { cout << "server listen fail" << endl; exit(3); } } void run() { //等待客户端 sockaddr_in client; socklen_t len = sizeof(client); int cilent_id = accept(server_fd,(struct sockaddr*)&client,&len); cout << "get a new link ...." << endl; while(true) { // 读取数据 char buff[1024]; int n = read(cilent_id, buff, sizeof(buff)); if (n < 0) { cout << "server read fail" << endl; } buff[n] = 0; cout << buff << endl; // 写入数据 string s = "server say : "; s += buff; write(cilent_id, s.c_str(), s.size()); } }private: int server_fd; uint16_t server_port; string server_ip;};
#include<iostream>#include<string>#include<unistd.h>#include <sys/socket.h>#include<sys/types.h>#include <netinet/in.h>#include <arpa/inet.h>using namespace std;int main(){ //创建套接字 int fd = socket(AF_INET,SOCK_STREAM,0); if(fd < 0) { cout << "client : socket fail" << endl; } //连接服务端 sockaddr_in server; string ip = "1.94.49.66"; int len = sizeof(server); server.sin_family = AF_INET; server.sin_port = htons(8080); server.sin_addr.s_addr = inet_addr(ip.c_str()); int n = connect(fd,(struct sockaddr*)&server,len); if(n < 0) { cout << "client : connent fail" << endl; } while(1) { //发送信息 string s; getline(cin,s); ssize_t m = write(fd,s.c_str(),s.size()); if(m < 0) { cout << "client : write fail" << endl; } //接收信息 char buff[1024]; m = read(fd,buff,sizeof(buff)); if(m < 0) { cout << "client : read fail" << endl; } buff[m] = 0; cout << buff << endl; }}
11. TCP协议通讯流程
建立连接的过程, 被称作 三次握手
断开连接的过程, 被称作 四次挥手
12. 守护进程
(一)了解守护进程
在操作系统中运行的一种特殊的后台进程,它们独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件
注意:
一个终端,前台进程只有一个,后台进程可以有很多个,且对于只有前台进程可以进行输入(键盘输入的信息都是发送给前台进程)执行一个进程后面 + & ,即可把它变成后台进程最开始的前台进程一般都是这个终端下的 bash 进程 , 如果再执行一个进程,会将这个进程变成前台进程,bash 进程变成后台进程如果一个终端退出,不代表这个终端下的后台进程会退出,它们会该改变 PPID 和 SID,即被操作系统管理linux 中,可以创建很多个终端,它们需要被管理 -------- 先描述再组织后台进程会受它所在的终端登入和退出的影响,而守护进程会单独弄出一个终端(要求这个后台进程不能是和进程组PGID一样)
(二)守护进程创建思路
思路一:
思路二:
int daemon(int nochdir, int noclose);
参数:
nochdir : 填写 0 或者 非0,如果为0,则改变工作路径为根目录,如果不为0,当前工作目录保持不变
noclose : 填写 0 或者 非0,如果为0,标准输入、标准输出和标准错误写入到/dev/null文件中
(三)查看后台进程
jobs
(四)与后台进程相关指令
后台进程转前台进程fg + 后台任务号
后台进程由 stop 状态变成 running 状态
bg + 后台任务号
注意:
Ctrl + Z : 可以使得前台进程暂停,此时前台进程无法执行,就会会到后台进程,bash 进程再一次成为前台进程