一、第一个网络通讯程序
- 网络通讯是指两台计算机中的程序进行传输数据的过程
- 客户程序(端):指主动发起通讯的程序。
- 服务程序(端/器):指被动的等待,然后为向它发起通讯的客户端提供服务。
/** 程序名:demo1.cpp,此程序用于演示socket的客户端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;int main(int argc,char *argv[])
{if (argc!=3){cout << "Using:./demo1 服务端的IP 服务端的端口\nExample:./demo1 192.168.101.139 5005\n\n"; return -1;}// 第1步:创建客户端的socket。 int sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd==-1){perror("socket"); return -1;}// 第2步:向服务器发起连接请求。 struct hostent* h; // 用于存放服务端IP的结构体。if ( (h = gethostbyname(argv[1])) == 0 ) // 把字符串格式的IP转换成结构体。{ cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1;}struct sockaddr_in servaddr; // 用于存放服务端IP和端口的结构体。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET;memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // 指定服务端的IP地址。servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) // 向服务端发起连接清求。{ perror("connect"); close(sockfd); return -1; }// 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。char buffer[1024];for (int ii=0;ii<3;ii++) // 循环3次,将与服务端进行三次通讯。{int iret;memset(buffer,0,sizeof(buffer));sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。// 向服务端发送请求报文。if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0){ perror("send"); break; }cout << "发送:" << buffer << endl;memset(buffer,0,sizeof(buffer));// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0){cout << "iret=" << iret << endl; break;}cout << "接收:" << buffer << endl;sleep(1);}// 第4步:关闭socket,释放资源。close(sockfd);
}
/** 程序名:demo2.cpp,此程序用于演示socket通信的服务端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;int main(int argc,char *argv[])
{if (argc!=2){cout << "Using:./demo2 通讯端口\nExample:./demo2 5005\n\n"; // 端口大于1024,不与其它的重复。cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";return -1;}// 第1步:创建服务端的socket。 int listenfd = socket(AF_INET,SOCK_STREAM,0);if (listenfd==-1) { perror("socket"); return -1; }// 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放服务端IP和端口的数据结构。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET; // 指定协议。servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,普通用户只能用1024以上的端口。// 绑定服务端的IP和端口。if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ){ perror("bind"); close(listenfd); return -1; }// 第3步:把socket设置为可连接(监听)的状态。if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }// 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。int clientfd=accept(listenfd,0,0);if (clientfd==-1){perror("accept"); close(listenfd); return -1; }cout << "客户端已连接。\n";// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。char buffer[1024];while (true){int iret;memset(buffer,0,sizeof(buffer));// 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。// 如果客户端已断开连接,recv()函数将返回0。if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) {cout << "iret=" << iret << endl; break; }cout << "接收:" << buffer << endl;strcpy(buffer,"ok"); // 生成回应报文内容。// 向客户端发送回应报文。if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; }cout << "发送:" << buffer << endl;}// 第6步:关闭socket,释放资源。close(listenfd); // 关闭服务端用于监听的socket。close(clientfd); // 关闭客户端连上来的socket。
}
输出:
[root@localhost 08demo_net]# g++ -o server demo2.cpp
[root@localhost 08demo_net]# g++ -o client demo1.cpp
[root@localhost 08demo_net]# ./server 5005
客户端已连接。
接收:这是第1个超级女生,编号001。
发送:ok
接收:这是第2个超级女生,编号002。
发送:ok
接收:这是第3个超级女生,编号003。
发送:ok
iret=0
[root@localhost 08demo_net]# ./client 127.0.0.1 5005
发送:这是第1个超级女生,编号001。
接收:ok
发送:这是第2个超级女生,编号002。
接收:ok
发送:这是第3个超级女生,编号003。
接收:ok
二、基于Linux的文件操作
// demo3.cpp,本程序演示了Linux底层文件的操作-创建文件并写入数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;int main()
{int fd; // 定义一个文件描述符/文件句柄。// 打开文件,注意,如果创建后的文件没有权限,可以手工授权chmod 777 data.txt。fd=open("data.txt",O_CREAT|O_RDWR|O_TRUNC);if (fd==-1){perror("open(data.txt)"); return -1;}printf("文件描述符fd=%d\n",fd);char buffer[1024];strcpy(buffer,"我是一只傻傻鸟。\n");if (write(fd,buffer,strlen(buffer))==-1) // 把数据写入文件。{perror("write()"); return -1;}close(fd); // 关闭文件。
}
// demo4.cpp,本程序演示了Linux底层文件的操作-读取文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd; // 定义一个文件描述符/文件句柄。fd=open("data.txt",O_RDONLY); // 打开文件。if (fd==-1){perror("open(data.txt)"); return -1;}printf("文件描述符fd=%d\n",fd);char buffer[1024];memset(buffer,0,sizeof(buffer));if (read(fd,buffer,sizeof(buffer))==-1) // 从文件中读取数据。{perror("write()"); return -1;}printf("%s",buffer);close(fd); // 关闭文件。
}
[root@localhost 09demo_file]# g++ -o demo3 demo3.cpp
[root@localhost 09demo_file]# g++ -o demo4 demo4.cpp
[root@localhost 09demo_file]# ./demo3
文件描述符fd=3
[root@localhost 09demo_file]# ./demo4
文件描述符fd=3
我是一只傻傻鸟。
- /proc/进程id/fd目录中,存放了每个进程打开的fd。
- Linux进程默认打开了三个文件描述符:0-标准输入(键盘),1-标准输出(显示器),2-标准错误(显示器)。
- 文件描述符的分配规则是:找到最小的,没有被占用的文件描述符。
查看进程打开的文件描述符
[root@localhost /]# cd /proc
[root@localhost proc]# ls
1 1388 16 22 29 36 415 50 6 689 acpi driver key-users mtrr sys
10 1391 1613 23 297 37 416 502 602 690 asound execdomains kmsg net sysrq-trigger
101 14 1627 24 298 38 417 51 604 691 buddyinfo fb kpagecount pagetypeinfo sysvipc
1066 1415 18 25 299 385 418 52 606 694 bus filesystems kpageflags partitions timer_list
1067 1439 1825 26 30 386 419 528 608 697 cgroups fs loadavg sched_debug timer_stats
1069 1445 1826 27 300 396 420 53 609 7 cmdline interrupts locks schedstat tty
11 1469 19 274 301 397 421 537 610 714 consoles iomem mdstat scsi uptime
12 1477 2 28 302 4 422 568 611 720 cpuinfo ioports meminfo self version
1216 1500 20 286 303 411 46 569 66 726 crypto irq misc slabinfo vmallocinfo
1226 1537 2073 287 305 412 48 570 663 763 devices kallsyms modules softirqs vmstat
1227 1547 21 288 306 413 49 571 687 8 diskstats kcore mounts stat zoneinfo
13 1591 2171 289 35 414 5 599 688 9 dma keys mpt swaps
[root@localhost proc]# cd 688
[root@localhost 688]# ls
attr cmdline environ io mem ns pagemap sched stack task
autogroup comm exe limits mountinfo numa_maps patch_state schedstat stat timers
auxv coredump_filter fd loginuid mounts oom_adj personality sessionid statm uid_map
cgroup cpuset fdinfo map_files mountstats oom_score projid_map setgroups status wchan
clear_refs cwd gid_map maps net oom_score_adj root smaps syscall
[root@localhost 688]# cd fd
[root@localhost fd]# ls
0 1 2
关闭标准输入标准输出
close(0);
close(1);
三、万恶的结构体
- sockaddr结构体
存放协议族、端口和地址信息,客户端和connect()函数和服务端的bind()函数需要这个结构体。
struct sockaddr {unsigned short sa_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。unsigned char sa_data[14]; // 14字节的端口和地址。
};
- sockaddr_in结构体
sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。
struct sockaddr_in { unsigned short sin_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。unsigned short sin_port; // 16位端口号,大端序。用htons(整数的端口)转换。struct in_addr sin_addr; // IP地址的结构体。192.168.101.138unsigned char sin_zero[8]; // 未使用,为了保持与struct sockaddr一样的长度而添加。};struct in_addr { // IP地址的结构体。unsigned int s_addr; // 32位的IP地址,大端序。};
- gethostbyname函数
根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。
struct hostent *gethostbyname(const char *name);struct hostent { char *h_name; // 主机名。char **h_aliases; // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。 short h_addrtype; // 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。short h_length; // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。char **h_addr_list; // 主机的ip地址,以网络字节序存储。 };#define h_addr h_addr_list[0] // for backward compatibility.
转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员中。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
4.字符串IP与大端序IP的转换
C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。
typedef unsigned int in_addr_t; // 32位大端序的IP地址。// 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp); // 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。
int inet_aton(const char *cp, struct in_addr *inp); // 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。
char *inet_ntoa(struct in_addr in);
/** 程序名:demo5.cpp,此程序用于演示socket的客户端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;int main(int argc,char *argv[])
{if (argc!=3){cout << "Using:./demo5 服务端的IP 服务端的端口\nExample:./demo5 192.168.101.138 5005\n\n"; return -1;}// 第1步:创建客户端的socket。 int sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd==-1){perror("socket"); return -1;}// 第2步:向服务器发起连接请求。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。servaddr.sin_port = htons(atoi(argv[2])); // ②指定服务端的通信端口。struct hostent* h; // 用于存放服务端IP地址(大端序)的结构体的指针。if ( (h = gethostbyname(argv[1])) == nullptr ) // 把域名/主机名/字符串格式的IP转换成结构体。{ cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1;}memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的IP(大端序)。//servaddr.sin_addr.s_addr=inet_addr(argv[1]); // ③指定服务端的IP,只能用IP,不能用域名和主机名。if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1) // 向服务端发起连接清求。{ perror("connect"); close(sockfd); return -1; }// 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。char buffer[1024];for (int ii=0;ii<10;ii++) // 循环3次,将与服务端进行三次通讯。{int iret;memset(buffer,0,sizeof(buffer));sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。// 向服务端发送请求报文。if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0){ perror("send"); break; }cout << "发送:" << buffer << endl;memset(buffer,0,sizeof(buffer));// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0){cout << "iret=" << iret << endl; break;}cout << "接收:" << buffer << endl;sleep(1);}// 第4步:关闭socket,释放资源。close(sockfd);
}
/** 程序名:demo6.cpp,此程序用于演示socket通信的服务端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;int main(int argc,char *argv[])
{if (argc!=2){cout << "Using:./demo6 通讯端口\nExample:./demo6 5005\n\n"; // 端口大于1024,不与其它的重复。cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";return -1;}// 第1步:创建服务端的socket。 int listenfd = socket(AF_INET,SOCK_STREAM,0);if (listenfd==-1) { perror("socket"); return -1; }// 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family=AF_INET; // ①协议族,固定填AF_INET。servaddr.sin_port=htons(atoi(argv[1])); // ②指定服务端的通信端口。servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。//servaddr.sin_addr.s_addr=inet_addr("192.168.101.138"); // ③指定服务端用于通讯的IP(大端序)。// 绑定服务端的IP和端口。if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1){ perror("bind"); close(listenfd); return -1; }// 第3步:把socket设置为可连接(监听)的状态。if (listen(listenfd,5) == -1 ) { perror("listen"); close(listenfd); return -1; }// 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。int clientfd=accept(listenfd,0,0);if (clientfd==-1){perror("accept"); close(listenfd); return -1; }cout << "客户端已连接。\n";// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。char buffer[1024];while (true){int iret;memset(buffer,0,sizeof(buffer));// 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。// 如果客户端已断开连接,recv()函数将返回0。if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) {cout << "iret=" << iret << endl; break; }cout << "接收:" << buffer << endl;strcpy(buffer,"ok"); // 生成回应报文内容。// 向客户端发送回应报文。if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) { perror("send"); break; }cout << "发送:" << buffer << endl;}// 第6步:关闭socket,释放资源。close(listenfd); // 关闭服务端用于监听的socket。close(clientfd); // 关闭客户端连上来的socket。
}
四、socket函数详解
人与人沟通的方式有很多种:书信、电话、QQ、微信。如果两个人想沟通,必须先选择一种沟通的方式,如果一方使用电话,另一方也应该使用电话,而不是书信。
协议是网络通讯的规则,是约定。
int socket(int domain, int type, int protocol);
成功返回一个有效的socket,失败返回-1,errno被设置。
全部网络编程的函数,失败时基本上都是返回-1,errno被设置。
1)domain通讯的协议家族
PF_INET IPv4互联网协议族。
PF_INET6 IPv6互联网协议族。
PF_LOCAL 本地通信的协议族。
PF_PACKET 内核底层的协议族。
PF_IPX IPX Novell协议族。
IPv6尚未普及,其它的不常用。
2)typ数据传输的类型
SOCK_STREAM 面向连接的socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。
SOCK_DGRAM 无连接的socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。
3)protocol最终使用的协议
在IPv4网络协议家族中,数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP,数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP。
本参数也可以填0。
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建tcp的sock
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 创建udp的sock只要参数没填错,基本上不会失败。
不过,单个进程中创建的socket数量与受系统参数open files的限制。(ulimit -a )
五、TCP和UDP
1)TCP和UDP的区别
TCP
a)TCP面向连接,通过三次握手建立连接,四次挥手断开连接; 面试的重点
b)TCP是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;
c)TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;
d)TCP只支持点对点通信;
e)TCP报文的首部较大,为20字节;
f)TCP是全双工的可靠信道。
UDP
a)UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功;
b)UDP以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;
c)UDP没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;
d)UDP支持一对一,一对多,多对一和多对多的通信;
e)UDP报文的首部比较小,只有8字节;
f)UDP是不可靠信道。
2)TCP保证自身可靠的方式
a)数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
b)到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;
c)超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;
d)滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方不会再发送数据;
e)失序处理:TCP的接收端会把接收到的数据重新排序;
f)重复处理:如果传输的分片出现重复,TCP的接收端会丢弃重复的数据;
g)数据校验:TCP通过数据的检验和来判断数据在传输过程中是否正确。
3)UDP不可靠的原因
没有上述TCP的机制,如果校验和出错,UDP会将该报文丢弃。
4)TCP和UDP使用场景
TCP 使用场景
TCP实现了数据传输过程中的各种控制,适合对可靠性有要求的场景。
UDP 使用场景
可以容忍数据丢失的场景:
视频、音频等多媒体通信(即时通信);
广播信息。
5)UDP能实现可靠传输吗?
这是个伪命题,如果用UDP实现可靠传输,那么,应用程序必须实现重传和排序等功能,非常麻烦,还不如直接用TCP。谁能保证自己写的算法比写TCP协议的人更牛?
六、主机字节序和网络字节序
- 大端序/小端序
如果数据类型占用的内存空间大于1字节,CPU把数据存放在内存中的方式有两种:
大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。
小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。
假设从内存地址0x00000001处开始存储十六进制数0x12345678,那么:
Bit-endian(按原来顺序存储)
0x00000001 0x12
0x00000002 0x34
0x00000003 0x56
0x00000004 0x78
Little-endian(颠倒顺序储存)
0x00000001 0x78
0x00000002 0x56
0x00000003 0x34
0x00000004 0x12
Intel系列的CPU以小端序方式保存数据,其它型号的CPU不一定。
操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件(socket也是文件描述符)。
这样的话,字节序不同的计算机之间传输数据,可能会出现问题。
- 网络字节序
为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。
C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:
uint16_t h to n s(uint16_t hostshort); // uint16_t 2字节的整数 unsigned short
uint32_t htonl(uint32_t hostlong); // uint32_t 4字节的整数 unsigned int
uint16_t ntohs(uint16_t netshort);
uint32_t n to h l(uint32_t netlong);
h host(主机);
to 转换;
n network(网络);
s short(2字节,16位的整数);
l long(4字节,32位的整数);
- IP地址和通讯端口
在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。
例如:192.168.190.134 3232284294 255.255.255.255
192 168 190 134
大端:11000000 10101000 10111110 10000110
小端:10000110 10111110 10101000 11000000
- 如何处理大小端序
在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换,只有向sockaddr_in结体成员变量填充数据时,才需要考虑字节序的问题。
推荐一个零声学院项目课,个人觉得老师讲得不错,分享给大家:
零声白金学习卡(含基础架构/高性能存储/golang云原生/音视频/Linux内核)
https://xxetb.xet.tech/s/3Zqhgt