ip协议
- IP主要有以下四个主要功能:
数据传送
寻址
路由选择
数据报文的分段
- IP的主要目的是为数据输入/输出网络提供基本算法,为高层协议提供无连接的传送服务.这意味着在IP将数据递交给接收站点以前不在传输站点和接收站点之间建立对话。它只是封装和传递数据,但不向发送者或接收者报告包的状态,不处理所遇到的故障
TCP协议
- TCP是重要的传输层协议,目的是允许数据同网络上的其他节点进行可靠的交换。它能提供端口编号的译码,以识别主机的应用程序,而且完成数据的可靠传输TCP 协议具有严格的内装差错检验算法确保数据的完整性TCP 是面向字节的顺序协议,这意味着包内的每个字节被分配一个顺序编号,并分配给每包一个顺序编号
UDP协议
- UDP也是传输层协议,它是无连接的,不可靠的传输服务.当接收数据时它不向发送方提供确认信息,它不提供输入包的顺序,如果出现丢失包或重份包的情况,也不会向发送方发出差错报文.由于它执行功能时具有较低的开销,因而执行速度比TCP快
Socket
- Linux中的网络编程通过Socket(套接字)接口实现,Socket是一种文件描述符
- 类型
套接字socket有三种类型:
1.流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连 接的通讯流。它使用了TCP协议。TCP保证了数据传输的正确性和顺序性
2. 数据报套接字( SOCK_DGRAM )数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议UDP。
3.原始套接字原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议的测试等
地址结构
- 通用型
//通用型 struct sockaddr { //Sa_family:地址族,采用“AF_xxx”的形式,如:AF_INET u_short sa_family; //Sa_data:14字节的特定协议地址 char sa_data[14]; };
- ipv4
//piv4 struct sockaddr_in { //16位地址类型 short int sin_family; //16位端口号 unsigned short int sin_port; //32位IP地址 struct in_addr sin_addr; //8字节填充 //servaddr.sin_addr.s_addr unsigned char sin_zero[8]; /* 填0 */ }; //编程中一般并不直接针对sockaddr数据结构操作,而是使用与sockaddr等价的sockaddr_in数据结构
struct in_addr { unsigned long s_addr; // S_addr: 32位的地址 }
地址转换
IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的是IP地址是由32位的整数表示的,为了转换我们可以使用下面两个函数:
int inet_aton (const char * cp,struct in_addr * inp )char * inet_ntoa (struct in_addr in)
函数里面 a 代表 ascii n 代表network.第一个函数表示将a.b.c.d形式的IP转换为32位的IP,存储在 inp指针里面。第二个是将32位IP转换为a.b.c.d的格式
字节序转换
- #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地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转
换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
htons
把unsigned short类型从主机序转换到网络序,服务器使用
htonl
把unsigned long类型从主机序转换到网络序服务器使用
ntohs
把unsigned short类型从网络序转换到主机序,客户机使用
ntohl
把unsigned long类型从网络序转换到主机序,客户机使用
Socket编程常用函数
- Socket
创建一个主动socket
- bind
用于绑定IP地址和端口号到socket
- connect
该函数用于绑定之后的client端与服务器建立连接
- listen(服务器专用)
设置能处理的最大连接要求,Listen()并未开始接收连线,只是设置socket为listen模式。
- accept(服务器专用)
用来接受socket连接。函数返回生产一个新的连接套接口,只能与当前客户连接。
- send
- recv
TCP模型
Socket建立
- int socket(int family, int type, int protocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1
对于IPv4,family参数指定为AF_INET
对于TCP协议,type参数指定SOCK_STREAM,表示面向流的传输协议如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议
protocol参数的介绍从略,指定为0即可
sockaddr_in
- bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
- 首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到不某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000
bind绑定
- int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号.bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起,使 sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号
struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
- 无法绑定
client终止时自劢关闭 socket 描述符, server 的 TCP 连接收到 client 发的 FIN 段后处于 TIME_WAIT 状态。 TCP 协议规定,主劢关闭连接的一方要处于 TIME_WAIT 状态,等待两个 MSL ( maximum segment lifetime )的时间后才能回到 CLOSED 状态,因为我们先 Ctrl-C 终止了 server ,所以 server 是主动关闭连接的一方,在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口。 MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同,在 Linux 上一般经过半分钟后就可以再次启劢 server 了。在server的TCP连接没有完全断开之前丌允许重新监听是丌合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址丌同,connfd对应的是不某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址丌同的多个socket描述符。
- 重复绑定
在 server 代码的 socket() 和 bind() 调用之间插入如下代码:int opt = 1;setsockopt ( listenfd , SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
listen
- #include <sys/socket.h>
int listen(int sockfd, int backlog);
listen 函数仅由 TCP 服务器调用,它完成两件事:
- 当 socket 函数创建一个套接字时,默认为一个主动套接字,也就是说它是一个将调用 connect 发起连接的客户套接字。listen 函数把一个未连接的套接字转换为一个被动套接字,指示内核应该接收指向该套接字的连接请求。
- 本函数的第二个参数规定内核应该为相应套接字排队的最大连接个数。
Accept
- int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
socklen_t len;
len = sizeof(cliaddr);
&len
三方插手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号.addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
- 如果给cliaddr参数传NULL,表示不关心客户端的地址
Connect
- int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
- 客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址.
- connect()成功返回0,出错返回-1
send,recv
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
flag参数:
0
MSG_DONTROUT(send)
MSG_OOB 带外数据
MSG_PEEK(recv)
MSG_WAITALL 等待所有数据
sendto,recvfrom
- ssize_t sendto(int socketid, void *buf, size_t len, int flags,struct sockaddr *to,socklen_t len);
- ssize_t recvfrom(int socketid, void *buf, size_t len, int flags,struct sockaddr *from,socklen_t *len);
socklen_t len;
len = sizeof(cliaddr);
&len
网络编程实例
- 服务器
#include <stdio.h> #include <sys/socket.h>//套接口头文件 #include <netinet/in.h>//字节序转换头文件 #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <ctype.h>//toupper头文件 #define SERVPORT 8000//端口大小 int main(int argc, char *argv[]) { int listen_fd,conn_fd;//服务器有两个套接口,监听套接口,accept会返回一个连接套接口 int n; socklen_t len;//accept,recvfrom第三个参数 char buffer[100];//缓冲区 struct sockaddr_in serveraddr,clientaddr;//定义地址类型ipv4(AF_INET) listen_fd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接口,参数地址类型,套接口类型,0 bzero(&serveraddr,sizeof(serveraddr));//初始化服务器地址 serveraddr.sin_family = AF_INET;//地址类型 serveraddr.sin_port = htons(SERVPORT);//端口号,字节序转换 serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//服务器可以为服务器任意网卡,客户机需要指定 //地址类型转换 bind(listen_fd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));//绑定自身套接口,服务器首地址,服务器地址长度 listen(listen_fd,1024);//监听套接口 while(1) { len = sizeof(clientaddr); conn_fd = accept(listen_fd,(struct sockaddr *)&clientaddr,&len);//accept返回一个连接套接口,参数监听套接口,客户机地址首地址,客户机地址长度 while(1) { len = sizeof(clientaddr); n = recvfrom(conn_fd,buffer,sizeof(buffer),0,(struct sockaddr *)&clientaddr,&len); if(n == 0) { printf("客户机已经关闭!\n"); break; } buffer[n] = '\0'; printf("接收到如下消息:\n"); printf("%s\n",buffer); for(int i = 0; i < n; i++) { buffer[i] = toupper(buffer[i]); }//大小写转换 //printf("%s\n",buffer); sendto(conn_fd,buffer,n,0,(struct sockaddr *)&clientaddr,len);//与recvfrom参数差不多,注意接受多少发送多少,地址长度不需要取地址 } close(conn_fd);//用完套接口得关闭 } close(listen_fd); return 0; }
- 客服机
#include <stdio.h> #include <sys/socket.h>//套接口头文件 #include <netinet/in.h>//字节序转换头文件 #include <string.h> #include <unistd.h> #include <stdlib.h> #include <arpa/inet.h>//inet_addr头文件 #define SERVPORT 8000//端口大小需要与服务器一样 int main(int argc, const char *argv[]) { int client_fd;//客户机只有一个套接口 int n; struct sockaddr_in serveraddr,clientaddr;//定义地址类型 char send_buf[100]; char recv_buf[100]; socklen_t len;//recvfrom第六个参数 len = sizeof(serveraddr); if(argc != 2) { printf("请输入服务器ip地址\n"); exit(-1); } client_fd = socket(AF_INET,SOCK_STREAM,0);//创建套接口 //初始化套接口 bzero(&serveraddr,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(SERVPORT); serveraddr.sin_addr.s_addr = inet_addr(argv[1]);//客户机指定套接口,inet_addr函数不需要字节序转换 connect(client_fd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));//连接服务器 while(fgets(send_buf,100,stdin) != NULL) { sendto(client_fd,send_buf,strlen(send_buf),0,(struct sockaddr *)&serveraddr,sizeof(serveraddr));//五个参数,客户机fd,缓冲区,缓存内容实际大小用strlen,0,服务器首地址,服务器地址长度 n = recvfrom(client_fd,recv_buf,sizeof(recv_buf),0,(struct sockaddr *)&serveraddr,&len);//注意最后一个参数取地址 recv_buf[n] = '\0'; fputs(recv_buf,stdout); } close(client_fd); return 0; }