一、知识提及
1.源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
2.端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用.
我们网络通信的本质就是进程间通信,不过是在不同的主机上。
IP地址能标识唯一的一台主机,端口号port可以用来标识主机上唯一的一个进程。
IP :Port = 标识全网唯一的一个进程。
用客户端ip:客户端端口号和服务端ip:服务端端口号的通信就是socket
pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
- 不是所有的进程都要网络通信,但是所有进程都要有pid。
- 系统和网络功能解耦
源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";
一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?
一个进程可以绑定多个端口号。
一个端口号不可以被多个进程绑定。
3.TCP协议
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
4.UDP协议
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
5.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分(把一个数据的高权值位放在地址较高的地方或把一个数据的低权值位放在地址较低的地方,这就是小端,反之就是大端), 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高权值字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
二、socket编程接口
1.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);
2.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结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
3.设计一个简单的UDP网络程序
netstat -nltp
查看网络连接
如何实现 整数IP和字符串IP 的相互转换?
设置一个ip结构体,每个成员都是一个一字节整数
struct ip { uint8_t part1; uint8_t part2; uint8_t part3; uint8_t part4; };
1.将整数转换为字符串
假如现在有一个整数IP地址, int src_ip = 123456789;
将它强转成为struct ip*类型, struct ip* p = (struct ip*)src_ip;
然后,string ip = to_string(p->part1) + "." + to_string(p->part2) + "." + to_string(p->part3) + "." + to_string(p->part4);
就可以转成字符串格式了。
2.将字符串转换为整数
假如现在有个字符串IP地址, string ip = "192.168.233.148";
将它切分成 "192" "168" "233" "148"
先定义一个32位整数, uint32_t IP;
取地址强转成struct ip*, struct ip* x = (struct ip*)&IP;
然后就可以
x->part1 = stoi("192");
依次类推,再强转成uint32_t类型,就可以得到一个整数IP。
但是,我们库里有相关的系统调用函数,就不需要我们手写了,我们明白其原理即可。
in_addr_t inet_addr(const char *cp);
端口号 [0~1024]基本上是系统内置的端口号,最好绑定1024以上的端口号,而1024以上的端口号有些也不能轻易绑定,比如mysql的3306端口号。
提问:客户端需要绑定socket吗?
答:要!只不过不需要用户显示的绑定!一般由OS自由随机选择!
一个端口号只能被一个进程bind,对server是如此,对client,也是如此!
client为了防止不同的应用争抢端口号发生冲突,通常是由OS分配。
client的port是多少,其实不重要,只要保证主机上的唯一性即可!
127.0.0.1:本地环回地址 ,通常用它来进行cs的测试
源码可以点击下面的地址查看
test512 · AoDong/Linux-test - 码云 - 开源中国 (gitee.com)
为我们这个Udp实现Windows做客户端,Linux做服务器的收发消息的程序
#include <iostream>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
const static uint16_t port = 8080;
const static std::string ip = "192.168.233.138";
int main()
{
std::cout << "hello socket!" << std::endl;
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "client create socket error" << std::endl;
exit(1);
}
struct sockaddr_in server;
//bzero(&server, sizeof(server));
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
int len = sizeof(server);
std::string message;
char buffer[1024];
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
struct sockaddr_in tmp;
int len1 = sizeof(tmp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len1);
if (s > 0)
{
buffer[s] = '\0';
//std::cout << buffer << std::endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
4.设计一个简单的TCP网络程序
int listen(int sockfd, int backlog); //listen for connections on a socket
//accept a connection on a socket
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
initiate a connection on a socket
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在服务器得到新连接时,单进程版服务就不太适用了,所以我们就要用到多进程,而多进程,我们的子进程就会继承两个文件描述符,ListenSock_和sockfd,我们在子进程中关不关闭ListenSock_,但是父进程强烈要求关闭sockfd。
hao
在执行程序时,在后面加个&,表明将当前进程设置为后台进程运行。
jobs:查看后台进程。
fg 任务号:将该后台进程提至前台进程
在当前前台进程运行时,在键盘输入ctrl+z暂停,就是说像当前运行前台进程发送了一个19号信号,SIGSTOP。它会被暂停中止,并自动转为后台进程,此时bash就自动被提至前台进程。因为在命令行中,前台进程要一直存在。
bg 任务号:将暂停的后台进程恢复为前台进程
SID:进程组
打开linux,系统会创建一个bash进程,和一个session(会话),session会以bash的pid为SID创建新的进程组。
如果使进程不受用户登录或者注销的影响,就可以 -- 守护进程化。
自成进程组 自成会话的进程,守护进程
NAME setsid - creates a session and sets the process group ID SYNOPSIS #include <sys/types.h> #include <unistd.h> pid_t setsid(void); DESCRIPTION setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new session (i.e., its session ID is made the same as its process ID). The calling process also becomes the process group leader of a new process group in the session (i.e., its process group ID is made the same as its process ID). The calling process will be the only process in the new process group and in the new session. Initially, the new session has no controlling terminal. For details of how a session acquires a controlling terminal, see credentials(7).
注:
- 这个进程不能是进程组的组长
守护进程的本质 -- 孤儿进程
static const std::string nullfile = "/dev/null"; void Daemon(const std::string &cwd = "") { // 1.忽略其它信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); signal(SIGSTOP, SIG_IGN); // 2.将自己变为独立的会话 if(fork() > 0) exit(0); setsid(); // 3.更改当前调用进程的工作目录 if(!cwd.empty()) chdir(cwd.c_str()); // 4.标准输入、输出和错误 重定向至/dev/null int fd = open(nullfile.c_str(), O_RDWR); if(fd >0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd); } }
但是我们系统中也提供了一个这个函数 Deamon
int daemon(int nochdir, int noclose);
代码:test517 · AoDong/Linux-test - 码云 - 开源中国 (gitee.com)
三、简单地谈一下TCP协议
TCP是全双工的,接收消息和发送消息是可以同时进行的。
TCP是一个传输控制协议