目录
一. 为什么要进行网络编程?
跨越计算机:
网络编程能够实现不同计算机之间的通信,而IPC主要用于同一台计算机上的进程之间通信。因此,网络编程可以应用于分布式系统、互联网等需要跨越不同计算机的场景。
扩展性:
网络编程支持多个客户端与一个服务器进行通信,可以实现服务端集中管理、多客户端同时连接的情况。这样可以更好地满足大规模并发访问的需求,提高系统的扩展性。
跨平台:
网络编程基于网络协议如TCP/IP,是一种标准化的跨平台通信方式。不同操作系统和编程语言都支持网络编程,因此可以实现不同平台之间的互联互通。
网络资源利用:
通过网络编程,可以利用互联网上的各种资源,如数据库、云存储、Web服务等。这样可以实现更丰富的功能和服务,增加系统的灵活性和功能性。
二. 网络编程的特点
分布式通信:
网络编程可以实现分布式系统中不同计算机之间的通信。通过网络协议,可以将任务和计算资源分布在不同的计算机上,实现协同工作和资源共享。
客户端-服务器模型:
网络编程通常采用客户端-服务器模型,其中服务器提供服务,而客户端请求并接收服务。服务器等待客户端的连接请求,并对其做出响应。
基于套接字:
网络编程使用套接字(Socket)作为通信的接口和机制。套接字提供了一组接口和方法,使得应用程序能够通过网络进行数据传输和通信。
网络协议:
网络编程依赖于各种网络协议,如TCP/IP协议栈。网络协议规定了数据封装和传输的方式,保证了数据的可靠传输、拥塞控制等功能。
并发和多线程:
网络编程需要处理同时存在多个客户端请求的情况。为了提高并发性能,通常使用多线程或多进程来处理多个客户端请求,实现并发处理和响应。
异步通信:
网络编程中常常涉及异步通信,即发送方和接收方不需要同步进行操作。异步通信可以提高系统的效率和响应速度。
三. 套接字(Socket)
套接字(Socket)是网络编程中用于实现网络通信的一种机制或接口。它提供了一组函数和方法,使得应用程序能够通过网络进行数据传输和通信。
套接字通过网络协议(如TCP/IP)在计算机之间建立连接,并在连接上进行数据的发送和接收。它可以被看作是网络中两个应用程序之间的端点,一个套接字位于发送方,另一个套接字位于接收方。
在TCP/IP协议中,一个进程通过IP地址和TCP或UDP端口号来标识自己,同一台计算机上不同的进程可以使用相同的IP地址但不同的端口号。当一个进程需要与另一个进程通信时,它需要创建一个Socket套接字来表示自己,并使用目标进程的IP地址和端口号来连接到对应的Socket套接字。这样,两个Socket套接字就组成了一个Socket pair来唯一标识这个连接。在网络编程中,我们可以使用Socket API来创建、配置、连接、发送和接收数据以及关闭Socket套接字等操作。
3.1 常用的通讯协议
3.1.1 TCP协议
TCP,即传输控制协议(Transmission Control Protocol),是一种网络传输协议,可以让我们的计算机和其他设备通过互联网彼此交流。
想象一下,你要给一个朋友发一封电子邮件。你将这封邮件分成小块,在每个小块上都写上编号。然后,你把这些小块一个一个地寄给你的朋友。你的朋友在收到邮件的小块后,按照编号的顺序将它们重新组装在一起,最终得到完整的邮件。
TCP就像是这样一个邮件系统。当你发送数据时,TCP会将数据分成一小块一小块的报文段,并为每个报文段添加编号。然后,TCP会确保这些报文段按照正确的顺序到达接收方,并在接收方重新组装这些报文段,以确保数据完整无误。
不仅如此,TCP还能确保数据的可靠传输。如果有一个报文段在传输过程中丢失了,TCP会负责重新发送这个报文段,以确保接收方可以正确地收到数据。而且,TCP还可以调整传输速度,以适应网络的拥塞情况,确保数据能够流畅地传输。
所以,TCP就像是一个可靠的邮递员,负责将你发送的数据安全、完整地交付给目标设备,让你可以在互联网上进行可靠的通信。
3.1.2 UDP协议
UDP,即用户数据报协议(User Datagram Protocol),是一种简单的网络传输协议,与TCP相比更加轻量级和灵活。
想象一下,你要给一个朋友发一张明信片。你写下了你要说的话,然后在明信片上写上你朋友的地址,最后把它扔进邮箱寄出去。你并不关心明信片是否被正确送达,也不需要确认你的朋友是否已经收到。
UDP就像这样一个明信片系统。当你发送数据时,UDP会将你的数据打包成一个个数据报,然后直接发送给接收方。它不会像TCP那样进行可靠的数据重传和顺序调整,也不会提供连接管理的功能。
UDP之所以被称为轻量级和灵活,是因为它不需要建立连接,也没有复杂的数据校验机制。它只是简单地将你的数据发送出去,尽可能快地到达目标设备,并让接收方自行处理接收到的数据。
因为UDP没有像TCP那样的可靠性保证,所以它适用于一些对实时性要求较高、可以容忍少量数据丢失的应用场景。例如,音频和视频流的传输、在线游戏的实时交互等。在这些情况下,速度和实时性更为重要,而数据的准确性可以稍微牺牲一些。
所以,UDP就像是一张速递的明信片,快速地将数据发送给目标设备,虽然不保证到达和完整性,但在某些特定的应用场景中非常实用。
3.1.3 TCP与UDP的比较区别
可靠性:TCP提供可靠的数据传输,确保数据按照正确的顺序到达目标设备,并能够进行丢失的重传。而UDP则是一种不可靠的传输协议,它不提供数据重传和顺序调整的机制。
连接性:TCP是面向连接的协议,它在通信双方之间建立了一个稳定的连接,然后才进行数据的传输。UDP则是无连接的协议,它不需要事先建立连接,可以直接将数据报发送给接收方。
效率和延迟:由于TCP提供了可靠性保证和连接管理机制,它在数据传输过程中会产生较多的额外开销,例如建立连接、数据校验等。这使得TCP相对于UDP来说,在效率和传输延迟方面略低一些。
适用场景:TCP适用于对数据传输的准确性和完整性要求较高的场景,例如文件传输、网页浏览等。而UDP则适用于对实时性要求较高、可以容忍少量数据丢失的场景,例如音频/视频流的传输、实时游戏等。
数据量限制:由于TCP面向连接且提供可靠性保证,它对数据大小的限制比较松散,可以适应大量的数据传输。而UDP在发送数据时有一个固定的最大数据报大小限制,超过这个限制则需要进行分片处理。
3.1.4 字节序
字节序(Byte Order)是指在计算机存储和表示多字节数据时,字节的顺序排列方式。常见的字节序有两种:大端序(Big Endian)和小端序(Little Endian)。
大端序(Big Endian):数据的高位字节存储在低地址,低位字节存储在高地址。
小端序(Little Endian):数据的低位字节存储在低地址,高位字节存储在高地址。
不同的计算机体系结构和处理器可能采用不同的字节序。对于网络通信来说,字节序的统一非常重要,以确保数据能够正确地被接收和解析。因此,在网络传输中,经常需要进行字节序的转换。
为了实现字节序的转换,可以使用以下函数
#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位短整数。 //如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回, //如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
四. Socket的服务器与客户端建立
4.1 网络编程的相关函数
4.1.1 socket函数
作用 创建套接字,用于通信 头文件 #include <sys/types.h>
#include <sys/socket.h>
函数 int socket(int domain, int type, int protocol); 参数 domain(指定通信域):
AF_INET IPV4使用
AF_INET6 IPV6使用
AF_UNIX 或 AF_LOCAL 本地通信使用
AF_PACKET 原始套接字使用
type(套接字类型):
SOCK_STREAM TCP使用
SOCK_DGRAM UDP使用
SOCK_RAW 原始套接字使用
protocol:
附加协议 如果没有附加协议 传 0 即可
返回值 成功:返回文件描述符
失败:返回-1
4.1.2 bind 函数
作用 将套接字和网络信息结构体绑定 头文件 #include <sys/types.h>
#include <sys/socket.h>
函数 int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数 sockfd :
socket函数返回的套接字
addr:
struct sockaddr_in
{
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 网络字节序的端口号 */
struct in_addr sin_addr; /* IP地址 */
};
addrlen:
addr的大小
返回值 成功:返回 0
失败:返回-1
4.1.3 listen函数
作用 将套接字设置成被动监听状态 头文件 #include <sys/types.h>
#include <sys/socket.h>
函数 int listen(int sockfd, int backlog); 参数 sockfd:
socket函数返回的套接字
backlog:
允许同时连接服务的客户端的个数(只要不是0就可以)
返回值 成功:返回 0
失败:返回-1
4.1.4 accept函数
作用 阻塞等待客户端连接,一旦有客户端连接,就会返回一个
新的套接字(文件描述符),专门用于和该客户端进行通信
头文件 #include <sys/socket.h>
函数 int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); 参数 sockfd:
socket函数返回的套接字
addr:
用来保存客户端网络信息结构体的首地址,
如果不关心客户端的信息,可以传NULL
addrlen:
addr长度,如果没有可以传NULL
返回值 成功:返回文件描述符 专门用于和该客户端通信
失败:返回-1
4.1.5 connect函数
作用 与服务器建立连接 头文件 #include <sys/types.h>
#include <sys/socket.h>
函数 int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数 sockfd:
socket函数返回的套接字
addr:
要连接的服务器的网络信息结构体
addrlen:
addr长度
返回值 成功:返回 0
失败:返回-1
五. 示例代码
5.1 服务端 与 客户端的建立
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> int main() { /*认识服务端的创建*/ struct sockaddr_in serveraddr; struct sockaddr_in clineaddr; memset(&serveraddr,0,sizeof(struct sockaddr_in)); memset(&clineaddr,0,sizeof(struct sockaddr_in)); /*---------------------------------------------------------------------------------*/ //1.创建一个套接字 int serverfd; //返回的网络描述符 serverfd = socket(AF_INET,SOCK_STREAM,0);//现在配的是IPv4,TCP协议 if(serverfd == -1) { perror("socket"); exit(-1); } /*---------------------------------------------------------------------------------*/ //2.根据网络描述符绑定IP和端口 //用另外一种结构体 serveraddr.sin_family = AF_INET; //地址族,现在是IPV4 //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序 serveraddr.sin_port = htons(8888); //将一个字符串IP地址转换为一个32位的网络序列IP地址 inet_aton("192.168.128.132",&serveraddr.sin_addr); //强制转换成sockaddr bind(serverfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in)); /*---------------------------------------------------------------------------------*/ //3.监听 listen(serverfd,10);//现在配的是最大连接个数为10 /*---------------------------------------------------------------------------------*/ //4.接收 int len = sizeof(struct sockaddr_in); int acceptfd = accept(serverfd,(struct sockaddr*)&clineaddr,&len); if(acceptfd == -1) { perror("accept"); } else { printf("connect ip is %s\n",inet_ntoa(clineaddr.sin_addr)); } /*---------------------------------------------------------------------------------*/ //5.接收read char buf[128] = {0}; int n_read = read(acceptfd,buf,128); if(n_read == -1) { perror("read"); } else { printf("server recv the data[%d] is %s\n",n_read,buf); } /*---------------------------------------------------------------------------------*/ //6.发送write char *str = "I'm server"; write(acceptfd,str,strlen(str)); /*---------------------------------------------------------------------------------*/ printf("connect\r\n"); return 0; }
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> int main() { /*认识客户端的创建*/ struct sockaddr_in clineaddr; memset(&clineaddr,0,sizeof(struct sockaddr_in)); /*---------------------------------------------------------------------------------*/ //1.创建一个套接字 int clinefd;//返回的网络描述符 clinefd = socket(AF_INET,SOCK_STREAM,0);//现在配的是IPv4,TCP协议 if(clinefd == -1) { perror("socket"); exit(-1); } /*---------------------------------------------------------------------------------*/ //2.连接 //用另外一种结构体 clineaddr.sin_family = AF_INET; //地址族,现在是IPV4 //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序 clineaddr.sin_port = htons(8888); //将一个字符串IP地址转换为一个32位的网络序列IP地址 inet_aton("192.168.128.132",&clineaddr.sin_addr); //强制转换成sockaddr if(connect(clinefd,(struct sockaddr *)&clineaddr,sizeof(struct sockaddr)) == -1) { perror("connect"); exit(-1); } /*---------------------------------------------------------------------------------*/ //3.发送write char *str = "I'm cline!"; write(clinefd,str,strlen(str)); /*---------------------------------------------------------------------------------*/ //4.接收read char buf[128] = {0}; int n_read = read(clinefd,buf,128); if(n_read == -1) { perror("read"); } else { printf("cline:recv the data[%d] is %s\n",n_read,buf); } /*---------------------------------------------------------------------------------*/ return 0; }
运行结果:
5.2 实现双机聊天
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> int main(int argc,char **argv) { /*实现双机聊天*/ struct sockaddr_in serveraddr; struct sockaddr_in clineaddr; memset(&serveraddr,0,sizeof(struct sockaddr_in)); memset(&clineaddr,0,sizeof(struct sockaddr_in)); /*--------------------------------------------------------------------------*/ //1.创建一个套接字 int serverfd;//返回的网络描述符 serverfd = socket(AF_INET,SOCK_STREAM,0);//现在配的是IPv4,TCP协议 if(serverfd == -1) { perror("socket"); exit(-1); } /*--------------------------------------------------------------------------*/ //2.根据网络描述符绑定IP和端口 //用另外一种结构体 serveraddr.sin_family = AF_INET; //地址族,现在是IPV4 //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序 serveraddr.sin_port = htons(atoi(argv[2])); //将一个字符串IP地址转换为一个32位的网络序列IP地址 inet_aton(argv[1],&serveraddr.sin_addr); //强制转换成sockaddr bind(serverfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in)); /*--------------------------------------------------------------------------*/ //3.监听 listen(serverfd,10);//现在配的是最大连接个数为10 /*--------------------------------------------------------------------------*/ //4.接收 int acceptfd; int n_read; char str[128] = {0}; char buf[128] = {0}; int len = sizeof(struct sockaddr_in); while(1) { //每次都接收新的客户端 acceptfd = accept(serverfd,(struct sockaddr*)&clineaddr,&len); if(acceptfd == -1) { perror("accept"); } else { printf("connect ip is %s\n",inet_ntoa(clineaddr.sin_addr)); } //创建子进程用于接收 if(fork() == 0) { while(1) { memset(buf,0,sizeof(buf)); n_read = read(acceptfd,buf,128); if(n_read == -1) { perror("read"); } else { printf("cline: %s\n",buf); } } } //用于发送功能 while(1) { memset(str,0,sizeof(str)); fgets(str,sizeof(str),stdin); write(acceptfd,str,strlen(str)); } } }
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> int main(int argc,char **argv) { /*实现双机的聊天*/ struct sockaddr_in clineaddr; memset(&clineaddr,0,sizeof(struct sockaddr_in)); /*--------------------------------------------------------------------------*/ //1.创建一个套接字 int clinefd;//返回的网络描述符 clinefd = socket(AF_INET,SOCK_STREAM,0);//现在配的是IPv4,TCP协议 if(clinefd == -1) { perror("socket"); exit(-1); } /*--------------------------------------------------------------------------*/ //2.连接 //用另外一种结构体 clineaddr.sin_family = AF_INET; //地址族,现在是IPV4 //端口号,htons是将整型变量从主机字节顺序转变成网络字节顺序 clineaddr.sin_port = htons(atoi(argv[2])); //将一个字符串IP地址转换为一个32位的网络序列IP地址 inet_aton(argv[1],&clineaddr.sin_addr); //强制转换成sockaddr if(connect(clinefd,(struct sockaddr *)&clineaddr,sizeof(struct sockaddr)) == -1) { perror("connect"); exit(-1); } /*--------------------------------------------------------------------------*/ int n_read; char str[128] = {0}; char buf[128] = {0}; while(1) { //创建子进程用于接收功能 if(fork() == 0) { while(1) { memset(buf,0,sizeof(buf)); n_read = read(clinefd,buf,128); if(n_read == -1) { perror("read"); } else { printf("server: %s\n",buf); } } } //用于发送功能 while(1) { memset(str,0,sizeof(str)); fgets(str,sizeof(str),stdin); write(clinefd,str,strlen(str)); } } }
运行结果: