网络编程的总结

一、网络概述

   最早的网络:电路交换网

        1、建立链接->使用链接->释放链接

        2、物理通路被通信双方独占

    计算机网络雏形及发展:

美国为了解决一些威胁,成立并研究网络:

发展阶段1:面向终端的计算机网络(核心是中心计算机 )

发展阶段2:现代计算机网络阶段(核心是通信网路)

发展阶段3:计算机网络标准化阶段(核心:通信协议标准化)

发展阶段4:网络互联与高速网络阶段(核心:更加稳定且高速)

网络分类:

局域网:(家、班级),主机数据量2-200+

城域网:(城市),连接的是公司之间的网络

广域网:(国家)

网络数据传输

分组交换的方式:

目的:

1、尽可能保证数据安全

2、保证数据稳定(分组)

交换方式—存储转发

节点收到分组,先暂时存储下来,再检查其首部,按照首部中的目的地址,找到合适的节点转发出去

网络分层及协议

网络分层:

应用层:应用程序间沟通的层

传输层:提供进程间的数据传送服务。负责传送数据,提供应用程序端到端的逻辑通信

网络层:提供基本的数据封包传送功能(ip)

链路层:负责数据帧的发送和接收(mac)

七层协议模型:

解释:

常用协议:

应用层:

http:超文本传输协议(网页)

tftp:简单文件传输协议

ftp:文件传输协议

DNS:域名解析协议

传输层:

udp:用户数据报协议

tcp:传输控制协议

网络层:

ip:网际协议

icmp:网际控制管理协议

链路层:

arp:地址解析协议(根据ip得到mac)

常用的地址概念

mac地址

MAC 地址,用于标识网络设备,类似于身份证号,且理论上全球唯一

组成:以太网内的 MAC 地址是一个 48bit 的值

查看:

linux下

window下

ip地址

ip地址,标识的是主机。IP地址局域网内唯一。

组成:

使用 32bit,由{网络 ID,主机 ID}两部分组成

子网 ID:IP 地址中由子网掩码中 1 覆盖的连续位

主机 ID:IP 地址中由子网掩码中 0 覆盖的连续位

修改:

1、图形界面

2、命令

ifconfig eth33 192.168.1.1 netmask 255.255.255.0

router add default gw 192.168.1.254

分类:

方式1:

A 类地址:默认 8bit 子网 ID,第一位为 0(1-127)

子网掩码:255.0.0.0 ip:0xxx xxxx 0000 0000 0000 0000 0000 0000 ip地址范围:1.0.0.0~127.255.255.255

B 类地址:默认 16bit 子网 ID,前两位为 10(128-191)

子网掩码:255.255.0.0 ip:10xx xxxx xxxx xxxx 0000 0000 0000 0000 ip地址范围:128.0.0.0~191.255.255.255

C 类地址:默认 24bit 子网 ID,前三位为 110(192-223)

子网掩码:255.255.255.0 ip:110x xxxx xxxx xxxx xxxx xxxx 0000 0000 ip地址范围:192.0.0.0~223.255.255.255

D 类地址:前四位为 1110,多播地址

E 类地址: 前五位为 11110,保留为今后使用

A,B,C 三类地址是最常用的

方式2:

特殊的ip地址:

回环地址:

127.0.0.1~127.255.255.254 中的任何地址都将回环到本地主机中(测试本机网路)

网段地址:

(子网 ID 不同的网络不能直接通信,如果要通信则需要路由器转发)

主机 ID 全为 0 的 IP 地址表示网段地址(eg:10.9.42.4/255.255.0.0---->10.9.0.0)

广播地址:

主机 ID 全为 1 的 IP 地址表示该网段的广播地址(eg:10.9.42.4/255.255.255.0---->10.9.42.255)

子网掩码:

(subnet mask)又叫网络掩码、地址掩码是一个 32bit 由 1 和 0 组成的数值,并且 1 和 0 分别连续

作用:指明 IP 地址中哪些位标识的是主机所在的子网以及哪些位标识的是主机号

特点:必须结合 IP 地址一起使用,不能单独存在

        IP 地址中由子网掩码中 1 覆盖的连续位为子网 ID,其余为主机 ID

子网掩码的表现形式

192.168.220.0/255.255.255.0

192.168.220.0/24

端口:

标识网络通信的进程,用来区分一个系统内的多个进程。

特点(最好是一个进程一个端口)。

1、对于同一个端口,在不同系统中对应着不同的进程

2、对于同一个系统,一个端口只能被一个进程拥有

3、一个进程拥有一个端口后,传输层送到该端口的数据全部被该进程接收,同样,进程送交传输层的数据也

通过该端口被送出

端口号:

端口使用端口号来标识网络进程,本质无符号的16位整形数据。

数据包的封装及拆解

数据包的封装发生在数据发送阶段,数据包的拆解发生在数据接收阶段。

凡是经过链路层的数据包都会增加或者去掉链路层头部(14byte)

凡是经过网络层的数据包都会增加或者去掉网络层头部(20byte或者24byte)

凡是经过传输层的数据包大部分增加或者去掉tcp、udp头部(udp:8byte或者tcp:20或者24byte)

网络程序开发

C/S结构:客户端与服务器

胖客户端瘦服务器

B/S结构:浏览器与服务器

瘦客户端胖服务器

二、udp通信

概念:多字节数据保存顺序

分类:

小端格式:将低位字节数据存储在低地址

大端格式:将高位字节数据存储在低地址

了解了大小端存储的方式,如何在代码中进行判断呢?下面介绍两种判断方式:
方式一:使用强制类型转换 - 这种法子不错
#include <iostream>
using namespace std;
int main()
{
 int a = 0x1234;
 //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
 char c = (char)(a);
 if (c == 0x12)
 cout << "big endian" << endl;
 else if(c == 0x34)
 cout << "little endian" << endl;
}
 方式二:巧用 union 联合体
#include <iostream>
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
 int a;
 char ch;
};
int main()
{
 endian value;
 value.a = 0x1234;
 //a和ch共用4字节的内存空间
 if (value.ch == 0x12)
 cout << "big endian"<<endl;
 else if (value.ch == 0x34)
 cout << "little endian"<<endl;
}

字节序转换函数

主机大部分都是小端存储,网络数据都是大端存储(不可更改)

解决问题:

不同主机及网络字节顺序不同,导致数据混乱。

发送数据时,将要发送的多字节数据从主机字节序转换成网络字节序

头文件: 
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
功能: 
    将 32 位主机字节序数据转换成网络字节序数据
参数: 
    hostint32:待转换的 32 位主机字节序数据
返回值: 
    成功:返回网络字节序的值

uint16_t htons(uint16_t hostint16);
功能: 
    将 16 位主机字节序数据转换成网络字节序数据
参数: 
    uint16_t:unsigned short int
    hostint16:待转换的 16 位主机字节序数据
返回值: 
    成功:返回网络字节序的值

接受数据时,将接受到的数据从网络字节序转换成主机字节序。

uint32_t ntohl(uint32_t netint32);
功能: 
    将 32 位网络字节序数据转换成主机字节序数据
参数: 
    uint32_t: unsigned int
    netint32:待转换的 32 位网络字节序数据
返回值: 
    成功:返回主机字节序的值
uint16_t ntohs(uint16_t netint16);
功能: 
    将 16 位网络字节序数据转换成主机字节序数据
参数: 
    uint16_t: unsigned short int
    netint16:待转换的 16 位网络字节序数据
返回值: 
    成功:返回主机字节序的值

地址转换函数

发送数据(字符串ip地址转换成4byte数据)

int inet_pton(int family,const char *strptr, void *addrptr);
功能: 
    将点分十进制数串转换成 32 位无符号整数
参数: 
    family :协议族(AF_INET)
    strptr:点分十进制数串
    addrptr:32 位无符号整数的地址
返回值: 成功返回 1 、 失败返回其它

接受数据(4byte数据转换成字符串ip地址)

const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
功能:
    将 32 位无符号整数转换成点分十进制数串
参数: 
    family :协议族  (AF_INET)
    addrptr :32 位无符号整数
    strptr:点分十进制数串
    len :strptr 缓存区长度(192.168.142.114)
    len 的宏定义
    #define INET_ADDRSTRLEN 16 //for ipv4
    #define INET6_ADDRSTRLEN 46 //for ipv6

UDP编程

udp协议:用户数据报协议,面向无连接的传输层协议。

UDP 特点:

1、相比 TCP 速度稍快些

2、简单的请求/应答应用程序可以使用 UDP

3、对于海量数据传输不应该使用 UDP

4、广播和多播应用必须使用 UDP

UDP 应用:

DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等

udp的编程架构

创建套接字

这里的family是协议族,有AF_INET,AF_INET6,PF_PACKET等。

type:是套接字类型(SOCK_STREAM(tcp套接字),SOCK_DGRAM(udp套接字),SOCK_RAM(原始套接字)等)

protocted:协议类别(0)默认0就可以

发送消息

可以这样使用:

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2、发送消息
    char buf[]="hello";
    struct sockaddr_in dstaddr;
    dstaddr.sin_family=AF_INET;
    dstaddr.sin_port=htons(8000);
    inet_pton(AF_INET,"10.9.42.232",(void *)&dstaddr.sin_addr);
    sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&dstaddr,sizeof(dstaddr));
    //3、关闭套接字
    close(sockfd);
    return 0;
}

上面是一个创建套接字并且发送数据给ip地址为10.9.42.232,端口号为8000的案例

下一个函数是接收函数

一般来说,客户端发送请求或者发送数据,服务器来接受消息后对消息进行处理,然后发出应答,那么服务器是如何进行接受数据,客户端又是怎么接受服务器发的应答呢?

接收函数的使用方式是这样的

ssize_t recvfrom(int sockfd, void *buf,size_t nbytes,int flags,
                 struct sockaddr *from, socklen_t *addrlen);
功能: 
    接收 UDP 数据,并将源地址信息保存在 from 指向的结构中
参数: 
    sockfd: 套接字
    buf:接收数据缓冲区
    nbytes:接收数据缓冲区的大小
    flags: 套接字标志(常为 0)
    from: 源地址结构体指针,用来保存数据的来源
    addrlen: from 所指内容的长度
注意: 
    通过 from 和 addrlen 参数存放数据来源信息
    from 和 addrlen 可以为 NULL, 表示不保存数据来源
返回值: 
    成功:接收到的字符数
    失败: -1

下面是一个服务器接收函数的案例,

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2、绑定
    struct sockaddr_in myaddr;
    myaddr.sin_family=AF_INET;
    myaddr.sin_port=htons(8000);
    inet_pton(AF_INET,"10.9.42.4",(void *)&myaddr.sin_addr);
    bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
    
    //3、接受消息
    char buf[512]="";
    recvfrom(sockfd, buf,sizeof(buf),0,NULL, NULL);
    printf("%s\n",buf);

    //4、关闭套接字
    close(sockfd);
    return 0;
}

UDP客户端与服务器

我认为,客户端和服务器的区别也就是

1、客户端一般是主动发消息的一方

2、服务器有绑定,而客户端没有

下面是一个客户端和服务器的例子

客户端:

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    
    struct sockaddr_in dstaddr;
    dstaddr.sin_family=AF_INET;
    dstaddr.sin_port=htons(8000);
    inet_pton(AF_INET,"10.9.42.232",(void *)&dstaddr.sin_addr);
    char buf[64]="";
    char rcvbuf[64]="";
    int i=0;
    while(1){
    
        //发送消息
        i++;
        sprintf(buf,"这是 %d 消息\n",i);
        sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&dstaddr,sizeof(dstaddr));
        
        //接受消息
        recvfrom(sockfd, rcvbuf,sizeof(rcvbuf),0,NULL, NULL);
        printf("%s\n",rcvbuf);
    }

    //关闭套接字
    close(sockfd);
    return 0;
}

服务器:

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int sockfd ;
void fun(int sig)
{
    close(sockfd);
    _exit(1);
}
int main()
{
    signal(2,fun);

    //创建套接字
    sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //绑定
    struct sockaddr_in myaddr;
    myaddr.sin_family=AF_INET;
    myaddr.sin_port=htons(8000);
    inet_pton(AF_INET,"10.9.42.4",(void *)&myaddr.sin_addr);
    bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
    char buf[512]="";
    char ip[16]="";
    //保存来源
    struct sockaddr_in srcaddr;
    socklen_t addrlen;
    while(1){
        //接受消息
        recvfrom(sockfd, buf,sizeof(buf),0,(struct sockaddr *)&srcaddr, &addrlen);
        //inet_ntop(AF_INET,(void *)&srcaddr.sin_addr.s_addr,ip,16);
        printf("  --->%s\n",buf);
        sendto(sockfd,"ok",sizeof("ok"),0,(struct sockaddr *)&srcaddr,addrlen);        
    }
    //关闭套接字
    close(sockfd);
    return 0;
}
1、不管是客户端还是服务器,有创建套接字(socket:套接字类给正确)
2、如果要发送消息,指定目的地(选择合适的地址结构体)
允许发送0长度数据
3、如果是要接受消息,建议绑定自己的地址(选择合适的地址结构体、来源可按需保存,如果保存来源,来源的长度需要取地址)
绑定:
自己指定ip:inet_pton(AF_INET,"10.9.42.4",(void *)&myaddr.sin_addr);
自动获取ip:mymyaddr.sin_addr.s_addr=htonl(INADDR_ANY);
4、建议程序关闭时,close(套接字);

三、TFTP编程

tftp:简单文件传输协议。

特点:1、基于udp(udp的cs架构和函数都可以使用)2、无用户有效性验证

传输格式:1、二进制octet 2、文本模式netascii

tftp通信过程及数据包格式

如图:客户端发出请求,服务器发送数据包,然后客户端每次接受一次回应一次ack,这个过程直到发完。

数据包的格式:

下载文件案例:

客户端先发出一个请求,

根据这个数据包格式来:因为我们需要下载文件到客户端所以发给服务器的数据包格式应该是读

故而如此组包

int len=sprintf(buf,"%c%c%s%c%s%c",0,1,"StopLove.lrc",0,"netascii",0);

其中的第二个1,代表的是可读权限,文件名选择需要下载的文件就好模式选择文本模式。

规定发送数据一次性发送512

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //创建文件用来保存下载的内容
    int fd =open("StopLove.lrc",O_RDWR|O_CREAT,0777);     
    //2、发送请求
    char buf[1024]="";//tftp下载文件请求
    int len=sprintf(buf,"%c%c%s%c%s%c",0,1,"StopLove.lrc",0,"netascii",0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(69);
    inet_pton(AF_INET,"10.9.42.232",(void *)&serveraddr.sin_addr);
    sendto(sockfd,buf,len,0,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
     //3、循环接收发送数据
    struct sockaddr_in temaddr;
    socklen_t temaddrlen=sizeof(temaddr);
    while(1){
        //3.1接受消息
        len=recvfrom(sockfd, buf,sizeof(buf),0,(struct sockaddr *)&temaddr, &temaddrlen);
        if(buf[1]==5){ //buf=05 xx ......(差错信息)
            printf("%s\n",buf+4);
            break;
        }
        else{ //buf=03 0n ......(文件数据)
            //写文件
            write(fd,buf+4,len-4);
            //3.2回复ack   
            buf[1]=4;//buf=04 01 ......(512)
            sendto(sockfd,buf,4,0,(struct sockaddr *)&temaddr,temaddrlen);
            //3.3退出循环
            if(len<516)
                break;    
        }        
    }
    //关闭套接字
    close(sockfd);
        close(fd);
    return 0;
}

这个里面的(struct sockaddr*)要转成或者形式是因为这个结构体的通用在网络上传输是这个形式。

运行结果:

上传文件案例

首先要知道这个和上一个的区别,这个是客户端发出请求,服务器做出ack应答,并不是直接发数据包,真实发送的数据包是客户端发送的,所以客户端接收到的是ack应答

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
        //打开文件用来读取上传内容
     int fd=open("StopLove.lrc",O_RDWR);
    if(fd<0)
        return 0;
        
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }        
    //2、发送请求
    char buf[1024]="";
    int len=sprintf(buf,"%c%c%s%c%s%c",0,2,"StopLove.lrc",0,"netascii",0);//组上传文件请求
    struct sockaddr_in serveraddr;
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(69);
    inet_pton(AF_INET,"10.9.42.232",(void *)&serveraddr.sin_addr);
    sendto(sockfd,buf,len,0,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    //3、循环接受数据
    struct sockaddr temaddr;
    socklen_t temlen=sizeof(temaddr);
    while(1){
        printf("---recvfrom before\n");
        recvfrom(sockfd,buf,sizeof(buf),0,&temaddr,&temlen);
        if(buf[1]==5)//buf=05 xx ...(差错信息)
        {
            printf("%s\n",buf+4);
            break;
        }
        else{//buf=04 xx (ack)
            //组装数据包的数据部分
            len=read(fd,buf+4,512);//buf= 04 xx ...(数据)
            printf("---read=len=%d----\n",len);
                        //组装数据包的数据包类型部分
            buf[1]=3;//buf= 03 xx ...(数据)
                        //组装数据包的数据包编号部分
            short n=ntohs(*((short *)(buf+2)));
            *(short *)(buf+2)=htons(n+1);//buf= 03 xx+1 ...(数据)
            //发送数据包
            sendto(sockfd,buf,len+4,0,&temaddr,temlen);
            if(len<512)
                    break;            
        }            
    }    
    close(sockfd);
        close(fd);
    return 0;
}

注意使用recvfrom函数的时候,最后一个参数的问题,一定要

socklen_t temlen=sizeof(temaddr);这个先赋值在使用,不然的话无法判断接受的大小,会出现错误

UDP的广播

概念:由一台主机向该主机所在子网内的所有的主机发送数据的方式

支持广播的有:udp(因为不需要连接)   还有原始套接字:是用来灵活组装数据包的

广播地址

定向广播地址:主机 ID 全 1

1、例:对于 192.168.0.0/16,其定向广播地址为 192.168.255.255

2、通常路由器不转发该广播

受限广播地址:255.255.255.255

路由器从不转发该广播

单播与多播的区别

多播:

对于广播,这里有一个案例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }


    //修改套接字选项为支持广播
    int optval=1;
    setsockopt(sockfd, SOL_SOCKET,SO_BROADCAST,(void *)&optval, sizeof(optval));
    //发送消息
    char buf[]="hello";
    struct sockaddr_in dstaddr;
    dstaddr.sin_family=AF_INET;
    dstaddr.sin_port=htons(8000);
    inet_pton(AF_INET,"10.9.42.255",(void *)&dstaddr.sin_addr);
    sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&dstaddr,sizeof(dstaddr));
    //关闭套接字
    close(sockfd);
 
    return 0;
}

setsockopt这个函数在后面我们也会用到。

UDP多播

概念:

数据的收发仅仅在同一分组中进行

特点:

1、多播地址标示一组接口(D类ip地址每一个都是一个组)224~239

2、多播可以用于广域网使用

3、在 IPv4 中,多播是可选的

多播地址ip和mac:

1、多播ip:

每一个D类ip都可以是一个组。

2、多播mac:

有25位固定(0x01 0x00 0x5e 0)

还有23位是由ip的后三个字节映射。

请注意的是:有可能出现mac重复的情况,解决:数据包可以到链路层,但是到网络层发现ip不同,直接丢弃包(多重验证)

下面是一个添加多播发送消息的案例

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2、绑定
    struct sockaddr_in myaddr;
    myaddr.sin_family=AF_INET;
    myaddr.sin_port=htons(8000);
    myaddr.sin_addr.s_addr=htonl(INADDR_ANY);//建议自动获取
    bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
    
    //3、加入组
    struct ip_mreq groupaddr;//多播地址结构体
    inet_pton(AF_INET,"224.0.0.1",(void *)&groupaddr.imr_multiaddr);//填充多播组ip
    //inet_pton(AF_INET,"10.9.42.4",(void *)&groupaddr.imr_interface);//填充要增加到多播组的ip
    groupaddr.imr_interface.s_addr=htonl(INADDR_ANY);//填充要增加到多播组的ip
    setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP, (void *)&groupaddr,sizeof(groupaddr));//加入组
        
    //4、接受消息
    char buf[512]="";
    recvfrom(sockfd, buf,sizeof(buf),0,NULL, NULL);
    printf("%s\n",buf);

    //关闭套接字
    close(sockfd);
    return 0;
}

四、Tcp编程

tcp概述:传输控制协议 

特点:

1、面向连接,安全可靠,纠错,错误重传,每一个数据包都有确认

2、建立连接,通信,释放连接

3、客户端主动连接,服务器被动连接

udp与tcp区别:

1、面向连接,面向无连接

2、可靠性tcp强于udp

3、tcp不支持广播多播,udp支持

4、效率udp强于tcp

5、传输海量数据使用tcp。

tcp的c/s架构

      

tcp函数实现通信:他有着以下几个步骤

1、创建套接字

2、连接服务器

3、发送消息

4、接收消息

下面是一个tcp客户端的案例

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //指定服务器的地址
    struct sockaddr_in serveraddr;
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8000);
    serveraddr.sin_addr.s_addr=inet_addr("10.9.42.232");
    //inet_pton(AF_INET,"10.9.42.232",(void *)&serveraddr.sin_addr);
    
    
    //连接服务器
    int ret=connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    if(ret != 0)
    {
        printf("connect error !\n");
        return -1;
    }
    char buf[1024]="";
    while(1){
        //发送消息
        scanf("%s",buf);
        send(sockfd,buf,strlen(buf),0);
        if(strcmp(buf,"bye")==0)
            break;
        //接受消息
        recv(sockfd,buf,sizeof(buf),0);
        printf("recv:%s\n",buf);    
    }
    //关闭套接字
    close(sockfd);
    return 0;
}

结果如图所示

那么tcp服务器是如何实现的呢?

请看我写的这个服务器案例

#include <stdio.h>
#include <string.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in
int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //2、绑定指定地址
    struct sockaddr_in serveraddr;
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8005);
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    
    //3、套接字从主动变被动,并且创建队列保存连接客户端(监听)
    listen(sockfd,10);
    //4、提取
    struct sockaddr_in cliaddr;
    socklen_t clilen=sizeof(cliaddr);
    printf("accept befroe\n");
    int fd_new=accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
    printf("accept after\n");
    //5、循环接受消息,并自动回复ok
    char buf[1024]="";
    int len;
    while(1){
        //接受消息
        len=recv(fd_new,buf,sizeof(buf),0);//如果客户端断开,会给服务器发送0长度数据包
        printf("recv:%s\n",buf);
        //回复ok
        send(fd_new,"ok",strlen("ok"),0);
        if(len==0)//如果收到了0长度数据包,说明客户端断开,服务器也跟着结束
            break;    
    
    }
    //6、关闭套接字
    close(fd_new);
    close(sockfd);
    return 0;
}

这样运行这个服务器代码就可以收到消息啦(当然现在只支持一个客户端连接,可以通过多进程或者多线程的方式来实现多个客户对他的连接)

下面是一个线程版本的服务器

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>//close
#include <sys/socket.h>//socket
#include <arpa/inet.h>//inet_pton
#include <netinet/in.h>// sockaddr_in

void * client_recv_send(void *arg){
    char *cmd=(char *)arg;
    char buf[1024]="";
    char ip[16]="";
    int len,fd_new;
    sscanf(cmd,"%d:%s",&fd_new,ip);
    while(1){
            
            //接受消息
            len=recv(fd_new,buf,sizeof(buf),0);//如果客户端断开,会给服务器发送0长度数据包
            
            printf("%s---->%s\n",ip,buf);
            //回复ok
            send(fd_new,"ok",strlen("ok"),0);
            if(len==0)//如果收到了0长度数据包,说明客户端断开,服务器也跟着结束
                break;    
            }
}
int main()
{
    //1、创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    //设置端口复用
    int optval=1;
    setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,(void *)&optval, sizeof(optval));
    
    //2、绑定指定地址
    struct sockaddr_in serveraddr;
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8005);
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    int ret = bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
    if(ret != 0)
    {
        perror("bind");
        return -1;
    }
    //3、套接字从主动变被动,并且创建队列保存连接客户端(监听)
    listen(sockfd,10);
    
    struct sockaddr_in cliaddr;
    socklen_t clilen=sizeof(cliaddr);
    int fd_new;
    char cmd[64]="";
    while(1){        
        //4、多次提取
        char ip[16]="";
        fd_new=accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
        inet_ntop(AF_INET,(void *)&cliaddr.sin_addr,ip,16);
        //如果accept函数阻塞解除,说明提取到了一个客户端,我们需要一边跟这个客户端循环收发消息,一边继续提取
        sprintf(cmd,"%d:%s",fd_new,ip);
        pthread_t pth;
        pthread_create(&pth,NULL,client_recv_send,cmd);
        pthread_detach(pth);
    }
    //6、关闭套接字
    close(fd_new);
    close(sockfd);
    return 0;
}

三次握手:

这个连接过程包括客户端connect到服务器accept的整个过程

对于确认号和序列号

确认号:对方下一次发的编号(对方上一次法宝的序列号+数据包长度(对于数据包长度的话如果是0的话默认加1,如果是其他的就加其他))

序列号:上一次对方的的确认号

四次挥手:

断开连接过程:从任意一方(客户端或者服务器)调用close函数开始,直到另一方也调用close函数,并且收到主动断开连接方的ack结束。

对于tcp知识点的总结

1、提问:为什么要建立三次链接呢为什么不是俩次

1、一个目的是为了防止SYN攻击,当然只是屏蔽掉了一部分

2、双方是相互的,可以理解为这是一个互相确认通信方式是否正常的一种方式,一开始客户端发出syn请求是告诉服务器:我有正常的发送能力,然后服务器给客户端发送syn+ack是为了告诉客户端我具有收数据的能力,并且我也有发数据的能力,最后需要客户端再回一个ack是为了告诉服务器:我也有收数据的能力,这样是不是就跟很好理解了

2、提问1、半连接状态是什么

客户端发送ack,然后服务端发送ack和syn,如果客户端没有及时发送ack的话这个阶段就是半连接状态

3、2、提问、连接包的序列号是随机的吗

不是

4、序列号:是对方上一次发送的确认号

确认号:是期望对方下一次的序列号,他的值是对方上一次的序列号+数据包长度

5、每一个数据包发送完都需要一个ack应答

断开连接阶段

1、为什么要进行四次挥手,三次挥手不可以嘛?

答:断开连接(发送FIN)是要有一方调用close函数。如果是是客户端主动close,那服务器收到断开连接会立刻回复ack,但是服务器要想发送FIN,得调用close。立刻发送ack和调用close中间是有时间差,无法合并为一次挥手。

2、写程序时,将套接字关闭,如果无端口复用,再次打开程序,程序绑定端口占用。

调用close关闭并不是真的关闭了,而是要等一段时间(2MSL),才是真正关闭。

1MSL=1数据包在网络上最长存活周期。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
一个简单的socket网络编程例子: 服务器代码: #include #include #include #include #pragma comment(lib,"ws2_32.lib") //这句话的意思是加载ws2_32.lib这个静态库 #define NETWORK_EVENT WM_USER+100 //如果你用mfc做开发,你可以点击菜单project-〉setting-〉link-〉object/library中添加这个静态库。 //如果你用c语言,你需要通过#pragma comment(命令来连接静态库 int main(int argc, char* argv[]){ HANDLE hThread = NULL; //判断是否输入了端口号 if(argc!=3){ printf("Usage: %sPortNumber\n",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[2]))==0){ printf("端口号有误!"); exit(-1); } WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ //高字节指定了次版本号,低字节指定了主版本号,两个字节加到一起,就是你想要的Winsock库的版本号了 printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET serverSocket; if((serverSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(port); //绑定 if(bind(serverSocket,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR){ printf("套接字绑定到端口失败!端口: %d\n",port); exit(-1); } //进入侦听状态 if(listen(serverSocket,SOMAXCONN)==SOCKET_ERROR){ printf("侦听失败!"); exit(-1); } printf("Server %d is listening......\n",port); SOCKET clientSocket[5],maxSocket;//用来和客户端通信的套接字 struct sockaddr_in clientAddress;//用来和客户端通信的套接字地址 memset(&clientAddress,0,sizeof(clientAddress)); int addrlen = sizeof(clientAddress); fd_set fd_read; int i=0; int j; char buf[4096]; char buff[4096]="exit"; while(1) { FD_ZERO(&fd_read); maxSocket=serverSocket; FD_SET(serverSocket,&fd_read); //FD_SET(clientSocket[i-1],&fd_read); for(j=0;j<i;j++) { FD_SET(clientSocket[j],&fd_read); if(maxSocket"); //gets(buff); if(select(maxSocket+1,&fd_read,NULL,NULL,NULL)>0) { if(FD_ISSET(serverSocket,&fd_read)) { if(buff=="") { if((clientSocket[i++]=accept(serverSocket,(sockaddr*)&clientAddress,&addrlen))==INVALID_SOCKET) { printf("接受客户端连接失败!"); exit(-1); } else { for(j=0;j5) { printf("超过最大客户端数"); exit(-1); } } else { int bytes; for(int k=0;k<i;k++) { if(FD_ISSET(clientSocket[k],&fd_read)) { bytes=recv(clientSocket[k],buf,sizeof(buf),0); if(bytes==-1) { //listen(serverSocket,SOMAXCONN); for (int l=k;l<i;l++) clientSocket[l]=clientSocket[l+1]; i--; } /*if(bytes==0) { //printf("fdsdf"); listen(serverSocket,SOMAXCONN); for (int l=k;l0) { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(clientAddress.sin_addr),buf); if(send(clientSocket[k],buf,bytes,0)==SOCKET_ERROR) { printf("发送数据失败!"); exit(-1); } } } } } } } //清理套接字占用的资源 WSACleanup(); return 0; } 客户端代码: #include #include #include #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]){ //判断是否输入了IP地址和端口号 if(argc!=4){ printf("Usage: %s IPAddress PortNumber\n",argv[1]); exit(-1); } //把字符串的IP地址转化为u_long unsigned long ip; if((ip=inet_addr(argv[2]))==INADDR_NONE){ printf("不合法的IP地址:%s",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[3]))==0){ printf("端口号有误!"); exit(-1); } printf("Connecting to %s:%d......\n",inet_ntoa(*(in_addr*)&ip),port); WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET sock,serverSocket; if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = ip; serverAddress.sin_port = htons(port); //建立和服务器的连接 if(connect(sock,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR) { printf("建立连接失败!"); exit(-1); } char buf[4096]; while(1){ printf(">"); //从控制台读取一行数据 gets(buf); if(send(sock,buf,strlen(buf),0)==SOCKET_ERROR){ printf("发送c数据失败!"); exit(-1); } int bytes; if((bytes=recv(sock,buf,sizeof(buf),0))==SOCKET_ERROR) { printf("接收c数据失败!\n"); exit(-1); } else { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(serverAddress.sin_addr),buf); } } //清理套接字占用的资源 WSACleanup(); return 0; }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值