网络编程套接字、网络字节序及用udp写客户端和服务端聊天程序

认识IP地址
IP协议有两个版本:IPV4和IPV6。
IPV4:IPV4版本的IP地址是4字节无符号整数。那么就存在IP地址资源匮乏的时候,这时可以采用两种方法:
DHCP:ip地址动态分配(应用层协议);
NAT: 地址替换;
但是这两种方法只是暂时的有IP地址,但并不能从本质上解决IP资源不够的问题。
IPV6:IPV6版本的IP地址是128字节无符号整数。这个可以从本质上解决IP资源匮乏问题,但是由于不向下兼容IPV4,所以一各大厂商不采用IPV6版本协议。
注:后文凡是提到IP协议,没有特殊说明,都默认为是IPV4协议。

  • IP地址在IP协议中,用来标识网络中不同主机的地址,唯一标识不同的主机;
  • 通常使用“点分十进制”的字符串表示IP地址,如192.168.0.1;用点分割的每一个数字表示一个字节,每个字节范围是0-255;255.255.255.255是广播地址,0.0.0.0是无效地址,这两个地址都使用不了;
    源IP地址和目的IP地址
    在IP数据包头部中,有两个IP地址,分别为源IP地址和目的IP地址。但是只凭借IP地址就可以完成通信吗?比如说用微信发消息,有了IP地址就能够把消息发到对方的机器的微信上吗?还需要有一个其他的标识来区分出这个数据要给哪个程序进行解析。这个标识就是端口号。
    端口号
    端口号是传输层协议的内容:
  • 端口号是一个2字节16位的无符号整数;(0-65535之间一个数字,0-1024不推荐使用)
  • 端口号用来标识一个进程,告诉操作系统,当前数据要交给哪一个进程来处理。(为什么不用pid=getpid()来标识一个进程,因为pid会变,如一个进程关闭再打开后,pid就会变,而一个进程的端口号不会变)
  • IP地址+端口号能够唯一标识网络上某一主机的某一进程;
    源端口号和目的端口号
    传输层协议(TCP和UDP)的数据段有两个端口号,分别为源端口号(数据是谁发)和目的端口号(数据发给谁)。
    传输层两个协议(TCP和UDP)
    传输层有2个协议:tcp(传输控制协议)和udp(用户数据报协议 )协议,两个协议各有不同的特点和应用场景,协议如何进行数据传输,取决于协议的应用场景和当前使用场景,那么两个协议的特点就十分重要。
    TCP协议:
    数据可靠传输,可连接,面向字节流传输。
    UDP协议:
    数据不可靠传输,不可连接,面向数据报传输。
    **可靠传输:**数据能够安全全部发送;(TCP不会丢弃数据,而UDP可能会丢弃数据)
    可连接: TCP连接就像打电话,你拨特定的号码,对方在线并接起电话,然后通话,通完之后再挂断,整个过程是相互联系的,缺一不可; UDP不可连接就像寄信,你寄过去信息,对方有没有收到不知道,而且对方回不回答也不知道,对方对你送消息也是一样。
    面向字节流:
    优点:收发数据比较灵活,可以分多次发送,多次接受;
    缺点:数据没有明显边界(分多次发送,造成数据没有明显边界),容易造成tcp粘包问题;
    面向数据报:
    缺点:数据发送有最大长度限制,如果发送数据超过最大长度限制,将会丢弃该数据,接受的时候一条一条接受,不能分割,如果接受的数据超过最大长度,将发送失败
    优点:由于数据有边界,不会造成tcp粘包问题;
    网络字节序----大小端
    cpu在对数据在内存中进行存取时的方向不同分出了大小端。
    0x04 03 02 01 (低位是01,高位是04)
    大端:低地址处存低位;
    小端:低地址处存高位;
    MIPS—RISC处理器 ----大端
    x86处理器 ----小端
    如何判断大小端:
//联合体判断大小端
#include<stdio.h>
union U
{
        int a;
        unsigned char c;
}u;
int main()
{
        u.a=1;
        if(u.c==1)
        {   
                printf("intel\n");
        }   
        else
        {   
                printf("motel\n");
        }   
        return 0;
}
//直接判断大小端
#include<stdio.h>
int main()
{
        int a=1;
        //定义一个int变量,强转后[0]是低地址,低地址是1是小端,低地址是0是大端
        if(((unsigned char*)(&a))[0]==1)
        {   
                printf("intel\n"); //小端
        }   
        else
        {   
                printf("motor\n"); //大端
        }   
        return 0;
}

内存中的多字节数据相对内存地址有大端和小端之分,磁盘文件中的多字节数据相对与文件中的偏移地址也有大端和小端之分,网络数据流也有大端小端之分, 那么如何定义⺴⽹网络数据流的地址呢?
1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
4.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
5.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
这里写图片描述
socket套接字编程
socket是一套接口,用于网络编程的接口,同时socket也是一个数据结构;
想要开始网络编程,就需要先创建一个套接字,也就是对网络编程来说,第一步永远是创建套接字,套接字创建成功后,才可以通过对套接字的操作,来完成网络上数据的传输。
网络通信是网络上两个主机上的进程间的通信,这两个主机有一个区分:一个主机为客户端,一个主机是服务端,并且永远是客户端向服务端发起请求。即服务端在明,客户端在暗。如qq:客户端十分多,腾讯的服务器不知道客户端是谁,在哪里,所以无法向客户端推送消息,但是腾讯服务器地址是固定的,端口是固定的,只要有人下载了客户端,这些服务端信息都已经封装在qq软件中。
udp网络编程步骤:
1.创建套接字(功能:建立与网卡的联系,协议版本的选择,传输层协议的选择)
2.为套接字绑定地址信息(ip地址,端口(port))
3.接受数据
4.发送数据
5.关闭socket描述符
1.创建套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域
		 AF_INET :ipv4协议
type: 套接字类型
		SOCK_STREAM 流式套接字
		SOCK_DGRAM  数据报套接字
protocol :协议类型 
		如果是0,则表示默认;流式套接字默认tcp协议,报式套接字默认udp协议
		流式套接字: IPPROTO_TCP 6 
		报式套接字:IPPROTO_UDP 17  
如:socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
返回值:成功:套接字描述符 
	   失败:-1						

2.为socket绑定地址信息
确定socket中信息(端口号和IP地址),即确定谁能够操作缓冲区。

 #include <sys/socket.h>
 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 参数: sockfd: socket描述符
 	   addr :socket绑定的地址
 	   addrlen :地址信息长度
 返回值:成功:0(网卡操作那个进程),失败 -1
 sockaddr结构:
 struct sockaddr {
               sa_f  amily_t sa_family;
               char        sa_data[14];
                }
虽然bind里参数是sockaddr,但是真正在基于IPV4编程时,使用的结构体是sockaddr_in;这个结构体里主要有三部分信息:地址类型,端口号,IP地址。
sockaddr_in在头文件#include<netinet/in.h>或#include<arpa/inet.h>中定义。该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下: 
structsockaddr_in{

short           sin_family;//AF_INET(地址族)PF_INET(协议族)

unsigned short  sin_port;/*Portnumber(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/

struct in_addr  sin_addr;//32位IP地址

unsigned char   sin_zero[8];//没有实际意义,只是为了跟SOCKADDR结构在内存中对齐*/

};
该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址:
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};   
in_addr用来表示一个IPV4的IP地址,其实是一个32位整数。   

客户端不推荐手动绑定地址信息 ,因为绑定有可能因为特殊原因失败,但是客户端具体使用哪个地址和端口都可以,只要能把数据发送出去,所以客户端程序不手动绑定地址,直至发送数据时,操作系统检测到socket没有绑定地址,会自动选择合适的地址和端口为socket绑定地址,这种数据一般不会出错。
3.接受数据(网卡从缓冲区把数据拷到数据态))

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
socket : socket描述符
buf:接受数据存储在buff 
len :想要接受多少字节数据
flags :如果是0:如果缓冲区没有数据,将一直等待,即阻塞等待 
src_addr :用于确定是哪一个对端发送的数据,即确定对端地址信息
addrlen : 地址信息长度 
返回值:成功:实际从缓冲区接受数据长度
       失败: -1

4.发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd : 发送数据的时候就是通过这个socket所绑定的地址来发送
buf :要发送的数据
flags : 0 默认阻塞式发送
dest_addr :数据要发送到的对端地址
addrlen :dest_addr地址信息长度  
返回值: 成功:实际发送数据长度
        失败 :-1

接下里将会用udp写一个简单的客户端和服务端之间的聊天程序:
首先:客户端代码:

//udp客户端
//1.创建套接字
//2.绑定地址信息
//3.发送数据
//4.接受数据
//5.关闭套接字

#include<stdio.h>
#include<errno.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>

int main()
{
        //1.创建套接字
        int sockfd=socket(AF_INET,SOCK_DGRAM,0);//udp协议
        if(sockfd<0)
        {   
                perror("socket errno");
                return -1; 
        }   
        //2.绑定地址信息

        while(1){    
                //3.发送数据 :服务端先发送数据
                //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                //              const struct sockaddr *dest_addr, socklen_t addrlen);
                struct sockaddr_in ser_addr;
                ser_addr.sin_family=AF_INET;
                ser_addr.sin_addr.s_addr=(inet_addr)("192.168.61.128");//inet_addr将字符串转换成网络字节序
                ser_addr.sin_port=(htons)(9000);//将端口号转换成2个字节网络字节序
                char buff[1024];
                memset(buff,0x00,1024);
                printf("please send:\n");
 scanf("%s",buff);
                socklen_t  len=sizeof(struct sockaddr_in);
                ssize_t ret=sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&ser_addr,len);
                if(ret<0)
                {
                        perror("sendto error");
                        close(sockfd);
                        return -1;
                }
                //接受数据
                // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                //                 struct sockaddr *src_addr, socklen_t *addrlen);
                memset(buff,0x00,1024);
                ret=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&ser_addr,&len);
                if(ret<0)
                {
                        perror("recvfrom error");
                        close(sockfd);  //在任何可能退出的地方关闭sockfd
                        return -1;
                }
                printf("%d\n",ret);
                //从哪个主机的那个端口接收的数据
                printf("[%s:%d:]say:%s\n",(inet_ntoa)(ser_addr.sin_addr), (ntohs)(ser_addr.sin_port),buff); 
                 //char *inet_ntoa (struct in_addr);   
                 //uint16_t ntohs(uint16_t netshort);
       }
        close(sockfd);  //关闭socket描述符
        return 0;
}

接下来是服务端代码:

//udp服务端代码
//1.创建套接字
//2.为套接字绑定地址信息
//3.接受数据
//4.发送数据
//5.关闭stocket描述符

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
int main()
{
        //1.创建套接字
        // int socket(int domain, int type, int protocol);
        int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//第三个参数是0也可以
        if(sockfd<0)
        {   
                perror("socket error");
                return -1; 
        }   
        //2.为socket绑定地址信息(绑定的是自己服务端主机IP和端口)
        // int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
        struct sockaddr_in ser_addr;
        ser_addr.sin_family=AF_INET;
        ser_addr.sin_port=(htons)(9000); //将端口转换成网络字节序
        ser_addr.sin_addr.s_addr=(inet_addr)("192.168.61.128");  //将点分十进制转换成网络字节序
        int len=sizeof(struct sockaddr_in);    
        int ret=bind(sockfd,(struct sockaddr*)&ser_addr,len);
        if(ret<0)
        {   
                perror("bind error");
                close(sockfd);
                return -1; 
 }

        while(1){
                //3.接收数据
                // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flag, struct sockaddr *src_addr, socklen_t *addrlen);
                char buff[1024]={0};
                struct sockaddr_in cli_addr;   //定义对端地址信息结构体
                len=sizeof(struct sockaddr_in);
                ret=recvfrom(sockfd,buff,1023,0,(struct sockaddr*)&cli_addr,&len);
                if(ret<0)
                {
                        perror("recvfrom error");
                        close(sockfd);
                        return -1;
                }
                //从哪个主机的那个端口接收的数据,这时cli_addr中对端地址信息
                //inet_ntoa:将网络字节序转换成字符串;ntohs:将网络字节序转换成2字节端口
                printf("[%s:%d]say:%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buff);
                //4.发送数据
                //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
                memset(buff,0x00,1024);
                scanf("%s",buff);
                ret=sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&cli_addr,len);
                if(ret<0)
                {
                        perror("sendto error");
                        close(sockfd);
                        return -1;
                }
        }
        close(sockfd);  //关闭socket描述符
        return 0;
}

服务端:
在这里插入图片描述
客户端:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/82720870