目录
②UDP(用户数据报协议,User Data Protocol)
⑥数据收发:read() write()和send() recv()
一,什么是网络编程
网络编程从大的方面说就是对信息的发送到接收,中间传输为物理线路的作用。网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理,代码、开发工具、数据库、服务器架设和网页设计这5部分都需要接触。
二,为什么使用端口号
①一台拥有IP地址的主机可以提供一个IP地址来实现。那么,主机时怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系;
②实际上是通过“IP地址+端口号”来区分不同服务的;
③端口提供了一种访问通道;
④服务器一般是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说。FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69;
三,TCP协议与UDP协议
①TCP(传输控制协议)
①提供IP环境下的数据可靠传输(一台计算机发出的字节流会无差错的发往网络上的其他计算机,而且计算机A接收数据包的时候,也会向计算机B回发数据包,这也会产生部分通信量),有效流控,全双工操作(数据在两个方向上能同时传递),多路复用服务,是面向连接,端到端的传输;
②面向连接:正式通信前必须要与对方建立连接。事先为所发送的数据开辟出连接好的通道,然后再进行数据发送,像打电话。
③TCP支持的应用协议:Telnet(远程登录)、FTP(文件传输协议)、SMTP(简单邮件传输协议)。TCP用于传输数据量大,可靠性要求高的应用。
②UDP(用户数据报协议,User Data Protocol)
1)面向非连接的(正式通信前不必与对方建立连接,不管对方状态就直接发送,像短信,QQ),不能提供可靠性、流控、差错恢复功能。UDP用于一次只传送少量数据,可靠性要求低、传输经济等应用。
2) UDP支持的应用协议:NFS(网络文件系统)、SNMP(简单网络管理系统)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。
③总结归纳
TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。
四,Socket服务器和客户端的开发流程
服务器端 | 客户端 |
①socket()。创建socket | ①socket()。创建socket |
②bind()。绑定IP地址、端口等信息到socket返回的描述符上 | ②结构体struct sockaddr_in设置要连接的对方的IP地址和端口等属性 |
③listen()。设置允许的最大连接数 | ③connect()。连接服务器 |
④accept()。接收客户端上来的连接 | ④read()和write(),send()和recv()。收发数据 |
⑤read()和write(),send()和recv()。收发数据 | ⑤close()。关闭网络连接 |
⑥close()。关闭网络连接 | |
注意:服务器端和客户端须使用相同的端口号和IP地址。 |
五,服务器和客户端相关API说明
①socket()函数
作用:创建socket套接字,获取类似文件描述符的网络描述符。Linux中的网络编程通过Socket(套接字)接口实现。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); //返回值:成功,则返回新套接字的文件描述符。失败返回-1,并设置errno。
▲ domain:指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);
● AF_INET IPv4因特网域;
● AF_INET6 IPv6因特网域;
● AF_UNIX Unix域;
● AF_ROUTE 路由套接字;
● AF_KEY 密钥套接字;
● AF_UNSPEC 未指定;
▲ type:指定socket的类型;
● SOCK_STREAM:流式套接字提供可靠的,面向连接的通信流;它使用TCP协议,从而保证数据传输的正确性和顺序性;
● SOCK_DGRAM: 数据报套接字定义一种无连接的服,数据通过相互独立的报文进行传输,是无序的,不能保证是可靠,无差错,它使用UDP数据报协议;
● SOCK_RAW:允许程序使用低层协议,原始套接字允许对底层协议如IP或ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。
▲ procolto:通常赋值‘0’,默认协议;
● "0"选择type类型对应的默认协议
● IPPROTO_TCP TCP传输协议
● IPPROTO_UDP UDP传输协议
● IPPROTO_SCTP SCTP传输协议
● IPPROTO_TIPC TIPC传输协议
②bind()函数
作用:绑定IP地址、端口等信息到socket返回的描述符上(socketfd)。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //返回值:如果成功,则返回0。发生错误时,返回-1。errno设置错误码。
▲ sockfd:socket返回的描述符(socketfd);
▲addr:是指向包含本机IP地址及端口号信息的sockaddr类型的指针,指向绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同。结构体如下:
▲addrlen:结构体大小sizeof();
sockaddr结构的两种原型:一般使用新版本struct sockaddr_in的结构体类型 struct sockaddr {
u_short sa_family; // 地址族或协议族,采用“AF_xxx”的形式,如:AF_INET
char sa_data[14]; // IP+端口, 14字节的特定协议地址
};对应IPv4,同等替换: struct sockaddr_in {
short int sin_family; /* 地址族或协议族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址结构体 */
unsigned char sin_zero[8]; /* 填充 ,无实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能互相转换*/};
struct in_addr{
unsigned long s_addr; // S_addr: 32位的地址
};linux结构体查询步骤:①cd /usr/include/ ②grep "struct sockaddr_in {" * -nir ③vi linux/in.h +显示的数字
③listen()函数
作用:监听套接字上的连接,设置能处理的最大连接数。不阻塞,告诉内核服务器能处理多少个连接队列。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); //返回值:如果成功,则返回0。发生错误时,返回-1和errno设置正确。
▲sockfd:socket返回的描述符(socketfd);
▲backlog:设置在请求连接队列中能连接的最大请求数,直接用写数字即可。
④accept()函数
作用:阻塞,等待接收客户端上来的连接。由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回值:该函数返回值是一个新的套接字描述符,返回值表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在,内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP三次握手已经完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。失败返回-1,设置erron
▲sockfd:服务器端调用socket返回的描述符(socketfd);
▲addr:用来返回已连接的对端(客户端)的协议地址;
▲addrled:客户端地址长度。
⑤客户端的connect()函数
作用:用于绑定后的client端(客户端),与服务器建立连接。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //返回值:如果连接或绑定成功,则返回0。失败,返回-1,并适当地设置errno。
▲socfd:服务端socket返回的描述符;
▲addr:服务器端IP地址和端口号的地址结构指针;(可以参考上面bind()函数中的结构指针);
▲addrlen:地址长度被设置为sizeof(struct socaddr);
⑥数据收发:read() write()和send() recv()
● read() write()
作用:在套接字通信中进行字节读取函数。与I/O中的读取函数略有区别,因为它们输入或输出的字节数可能比请求的数少。
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); //返回值:成功返回读或写的字节个数,出错返回-1
说明:第一个将buf中的nbytes个字节写入到文件描述符fd中,成功时返回写的字节数;第二个从fd中读取nbytes个字节到buf中,返回实际所读的字节数。
● send() recv()
作用:在TCP套接字上发送和接收数据函数:有连接
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); //包含3要素:套接字s,待发数据msg,数据长度len; //函数只能对处于连接状态的套接字使用,参数s为已读建立好连接的套接字描述符,即accept函数的返回值; //参数msg指向存放待发送数据的缓冲区; //参数len为待发送数据的长度,参数flag为控制选项,一般设置为0; ssize_t recv(int sockfd, void *buf, size_t len, int flags); //包含3要素:套接字s,接收缓冲区buf,长度len; //函数recv从参数s所指定的套接字描述符(必须面向连接的套接字)上接收数据并保存到buf所指定的缓冲区; //参数len为缓冲区长度,参数flags为控制选项,一般设置为0;
六,地址格式转换相关API
①ip地址字符串和网络格式转换(inet_aton)
说明:IP地址通常由数字加点(192.168.0.0)的形式表示,而在struct in_addr中使用的是IP地址是由32位的整数表示的,转换函数如下:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp,struct in_addr *inp) //将字符串形式的“000.000.0.0”形式的IP转换为32位的IP(网络能识别的格式),存储在inp指针里面。 char *inet_ntoa(struct in_addr in) //是将32位IP(网络格式)转换为字符串“000.000.0.0”的格式
②端口字节序转换(htons)
说明:不同类型的 CPU 对变量的字节存储顺序可能不同:有的系统是高位在前,低位在后,而有的系统是低位在前,高位在后,而网络传输的数据顺序是一定要统一的。所以当内部字节存储顺序和网络字节顺序不同时,就一定要进行转换。网络字节顺序采用big endian(大端)排序方式。
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong);//把unsigned long类型从主机序转换到网络序 uint16_t htons(uint16_t hostshort);//把unsigned short类型从主机序转换到网络序// uint32_t ntohl(uint32_t netlong);//把unsigned long类型从网络序转换到主机序 uint16_t ntohs(uint16_t netshort);//把unsigned short类型从网络序转换到主机序
函数说明:①h表示host,n表示network,l表示long(4字节),s表示short(2字节)。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
②根据情况使用NADDR_ANY,INADDR_ANY指定地址让操作系统自己获取。
七,socket服务端和客户端的代码实现
①socket服务端代码(无客户端连接):
#include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main(void) { //1 socket 创建套接字,获取描述符 int socfd = socket(AF_INET,SOCK_STREAM,0); if(socfd == -1){ perror("error socket"); } //结构体struct sockaddr_in设置要连接的对方的IP地址和端口等属性 struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(7777); inet_aton("127.0.0.1",&s_addr.sin_addr); //2 bind 绑定IP地址、端口等信息到socket返回的描述符上 bind(socfd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in)); //3 listen 设置允许的最大连接数 listen(socfd,10); //4 阻塞 等accept接收客户端上来的连接请求 struct sockaddr_in c_addr; int clen = sizeof(struct sockaddr_in); int c_fd = accept(socfd,(struct sockaddr *)&c_addr,&clen); if(c_fd == -1){ perror("error accept!\n"); } //5 read 读取来自客户端上的请求信息 char readBuf[128] = "masseag from client!"; int n_read = read(c_fd,readBuf,128); if(n_read == -1){ perror("read\n"); }else{ printf("read%dByte:%s\n",n_read,readBuf); } //6 write 收到客户端的请求信息后,回复信息给客户端 char *msg = "I got your masseag!"; write(c_fd,msg,strlen(msg)); //7 关闭网络连接 close(c_fd); return 0; }
编译结果:telnet 远程登陆协议
②客户端连接服务器端:
● 服务器端代码(server):
#include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> int main(void) { //1,socket 创建套接字,获取描述符 int sockfd = socket(AF_INET,SOCK_STREAM,0); if(sockfd == -1){ perror("socket"); exit(-1); } //2,bind 绑定IP地址和端口信息 struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(6666);//字节序转换 inet_aton("127.0.0.1",&s_addr.sin_addr);//IP地址格式转换 bind(sockfd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in)); //3,listen 设置监听数量,告诉内核本服务器能连接客户段的最大数量 listen(sockfd,1); //4,accept 接受套接字的连接 int clen = sizeof(struct sockaddr_in); int c_fd = accept(sockfd,(struct sockaddr *)&s_addr,&clen); if(c_fd == -1){ perror("accept"); exit(-1); } //5,read 服务器会先读到来至客户端的请求 char readBuf[128]; int n_read = read(c_fd,readBuf,128); if(n_read == -1){ perror("read"); }else{ printf("read%dByte:%s\n",n_read,readBuf); } //6,write 服务器在收到客户端的请求后,会回信给客户端 char *msg= "ok,I got your message!"; write(c_fd,msg,strlen(msg)); //7,关闭网络连接 close(c_fd); return 0; }
● 客户端代码(client):
#include <stdio.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> int main(void) { //1,socket 创建套接字,获取描述符 int sockfd = socket(AF_INET,SOCK_STREAM,0); if(sockfd == -1){ perror("sockfd"); exit(-1); } //结构体struct sockaddr_in设置要连接的对方的IP地址和端口等属性 struct sockaddr_in s_addr; s_addr.sin_family = AF_INET; s_addr.sin_port = htons(6666); inet_aton("127.0.0.1",&s_addr.sin_addr); //2,connect 连接服务器 if(connect(sockfd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr)) == -1){ perror("connect"); } //3,向服务器发送请求 char buf[128] = "I set your message"; write(sockfd,buf,strlen(buf)); //4,读取来自服务器的回信 char msg[128]; int n_read = read(sockfd,msg,128); if(n_read == -1){ perror("read"); }else{ printf("read%dByte:%s\n",n_read,msg); } //5,关闭网络连接 close(sockfd); return 0; }
● 编译结果: