网络编程(TCP/UDP)

前言

当我们进行网络编程的时候,只需要关注于对方进程的ip地址和端口号(0-1024为知名端口,用户不能随便使用,1024-4096为保留端口,用户也不能随便使用,端口号必须4096以上才可以用)就可以了。

API接口函数

网络编程接口

所需头文件: #include<sys/socket.h> //接口函数
int socket(int domain, int type, int protocol) :创建套接字
domain:设置套接字的协议簇,AF_UNIX AF_INET AF_INET6
type:设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM
protocol:一般设置为0,表示使用默认协议
返回值:成功返回套接字的文件描述符,失败返回-1

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):将 sockfd 与一个socket地址绑定。
sockfd:是网络套接字描述符
addr:地址结构
addrlen:socket 地址的长度
返回值:成功返回0,失败返回-1

int listen(int sockfd, int backlog):创建一个监听队列以存储待处理的客户连接。
sockfd:被监听的socket套接字 backlog:表示处于完全连接状态的 socket的上限(一般写5)
返回值:成功返回0,失败返回-1

int accept(int sockfd, struct sockaddr*addr, socklen_t *addrlen):accept()从 listen 监听队列中接收一个连接。
sockfd:是执行过 listen系统调用的监听socket
addr:用来获取被接受连接的远端socket地址
addrlen:该socket地址的长度
返回值:成功返回一个新的连接socket,该socket 唯一地标识了被接收的这个连接,失败返回-1

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen):客户端需要通过此系统调用来主动与服务器建立连接,
sockfd:由socket返回的一个socket。
serv_addr:服务器监听的socket地址
addrlen:这个地址的长度
返回值:成功返回0,失败返回-1

int close(int sockfd):关闭一个连接,实际上就是关闭该连接对应的socket。
sockfd:由socket返回的一个socket。

主机字节序列转换网络字节序列接口

所需头文件: #include<netinet/in.h>
uint32_t htonl( uint32_t hostlong ):长整型的 主机字节序列转换为长整型的 网络字节序列
hostlong:长整型的主机字节序列

uint32_t ntohl( uint32_t netlong ):长整型的 网络字节序列转换为长整型的 主机字节序列
netlong:长整型的网络字节序列

uint16_t htons( uint16_t hostshort ):短整型的 主机字节序列转换为短整型的 网络字节序列
hostshort:短整型的主机字节序列

uint16_t ntohs( uint16_t netshort ):短整型的 网络字节序列转换为短整型的 主机字节序列
netshort:短整型的网络字节序列

ip地址转换接口

所需头文件:#include<arpa/inet.h>
in_addr_t inet_addr( const char* cp ):字符串表示的ipv4地址转换位为网络字节序列
cp:字符串表示的ipv4地址(例如:192.168.0.1)
char* inet_ntoa( struct in_addr in ):ipv4地址的网络字节序列转换为字符串表示
in:存放数据为32位无符号整形的结构体变量

数据接收发送接口

ssize_t recv(int sockfd, void *buff, size_t len, int flags):读取sockfd上的数据,buff和 len参数分别指定读缓冲区的位置和大小

ssize_t scnd(int sockfd, const void *buff, size_t len, int flags):往socket上写入数据,buff和len参数分别指定写缓冲区的位置和数据长度flags参数为数据收发提供了额外的控制

TCP编程

编程流程:

服务器端:

1. socket()
2. bind()
3. listen():backlog的最大值不能超过128,它的值加1则是全连接队列的大小。
4. accept()
5. recv()
6. send()
7. close()

客户端:

1.socket()
2.connect() //发起连接,开始三次握手
3.send()
4.recv()
5.close()

监听队列及三次握手的建立过程

三次握手

首先了解一下TCP报头

四次挥手

三次握手四次挥手相关问题

TIME_WAIT状态的意义?

答:

确保可靠的终止TCP。当服务器发送FIN报文给客户端时,客户端回复ack确认报文在传输途中丢失,服务器就会再次发送FIN报文直到接收到ack确认报文终止TCP连接。

确保迟到的TCP报文段被识别并丢弃。当客户端给服务器发送了一段消息,但此时客户端突然结束,如果不清理这段报文,当我们再次用相同的ip和端口打开客户端连接服务器时,我们再发消息,就会发现消息不是这次发送的,因此需要TIME_WAIT等待一段时间去处理迟来的报文。

为什么断开连接等待时间是2MSL?

(MSL:报文在网络中最长的存活时间)

答:

因为网络有不确定因素所以有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文,也就是2MS,2MSL就是一个发送和一个回复所需的最大时间。如果客户端ACK丢失,导致服务器没有收到ACK,那么服务端将不断重复发送FIN片段。所以客户端不能立即关闭,它必须确认服务端已经接收到了该ACK。客户端会在发送出确认ACK之后进入到TIME_WAIT状态,它会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。如果直到2MSL,客户端都没有再次收到FIN,那么客户端推断ACK已经被成功接收,则结束TCP连接。

客户端链接服务端成功的条件

1.端口,ip地址,服务类型都正确

2.服务器正在运行

3.网络正常

4.服务器资源充足

多线程实现服务端并发

服务器端:

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

void* work_thread(void* arg)  //线程函数
{
    int c = (int)arg;  //套接字文件描述符
    while(1)
    {
        char buff[128] = {0};
        int n = recv(c,buff,127,0);  //接收客户端的消息
        if(n <= 0)  //没有收到消息退出
        {
            printf("client %d over\n",c);
            break;
        }
        printf("buff:%s\n",buff);
        send(c,"OK",2,0);   //接收并打印后回复客户端OK
    }
    close(c);   //关闭套接字文件描述符
}

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  //创建套接字
    assert(sockfd != -1);
    struct sockaddr_in saddr,caddr;   //定义套接字地址的结构体
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);   //端口号,短整型主机字节序列转为网络字节序列
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //字符串转为网络字节序列

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));  //绑定套接字
    assert(res != -1);
    listen(sockfd,5);   //创建监听队列并放入

    while(1)
    {
        int len = sizeof(saddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);  //接收连接,返回新的套接字
        if(c <= 0)
        {
            printf("accept err\n");
            break;
        }
        printf("c = %d ip = %s port = %d accepted\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));

        pthread_t id;
        pthread_create(&id,NULL,work_thread,(void*)c);   //创建线程
    }
}

客户端:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  //定义套接字
    assert(sockfd != -1);
    struct sockaddr_in saddr;   //定义套接字地址的结构体
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);  //端口号,短整型主机字节序列转为网络字节序列
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//字符串转为网络字节序列

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));  //发起连接,开始三次握手
    assert(res != -1);
    while(1)
    {
        printf("input: ");
        char buff[128] = {0};
        fgets(buff,128,stdin);
        send(sockfd,buff,strlen(buff)-1,0);  //向服务器发送数据
        if(strncmp(buff,"end",3) == 0)
        {
            break;
        }
        memset(buff,0,128);
        recv(sockfd,buff,127,0);   //接收服务其的消息
        printf("read: %s\n",buff);
    }
    close(sockfd);  //关闭套接字文件描述符
}

结果演示:

多进程实现服务端并发

服务器端:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);  //创建套接字
    assert(sockfd != -1);
    struct sockaddr_in saddr,caddr;   //定义套接字地址的结构体
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);   //端口号,短整型主机字节序列转为网络字节序列
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//字符串转为网络字节序列

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定套接字
    assert(res != -1);
    listen(sockfd,5);    //创建监听队列并放入

    while(1)
    {
        int len = sizeof(saddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);  //接收连接,返回新的套接字
        if(c <= 0)
        {
            printf("accept err\n");
            break;
        }
        printf("c = %d ip = %s port = %d accepted\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
        pid_t id = fork();   //进程复制
        if(id == 0)  //子进程
        {
            while(1)
            {
                char buff[128] = {0};
                int n = recv(c,buff,127,0);  //接收客户端消息
                if(n <= 0)
                {
                    printf("client %d over\n",c);
                    break;
                }
                printf("buff:%s\n",buff);
                send(c,"OK",2,0);  //回复OK
            }
        }
        close(c);
    }
}

客户端:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    assert(sockfd != -1);
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); //发起连接,开始三次握手
    assert(res != -1);
    while(1)
    {
        printf("input: ");
        char buff[128] = {0};
        fgets(buff,127,stdin);
        send(sockfd,buff,strlen(buff)-1,0);
        if(strncmp(buff,"end",3) == 0)
        {
            break;
        }
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("read: %s\n",buff);
    }
    close(sockfd);
}

结果演示:

send/recv缓冲区

TCP流式服务

TCP粘包问题

因为TCP流式服务的特点,导致会发生粘包问题(多次发送的数据会被一次收到)

例如我们连续发送:长 4宽 2高 5,我们将数据放入发送缓冲区中,再发送到接收缓冲区中,但我们直接读出425作为长,此时服务器会阻塞,等待我们发送宽和高。

如何解决TCP粘包问题:

①我们采用发送-回应的方式,我们发一次send,recv一次回复再去send

②定义发送接收的形式,例如[4][2][5],将'['视作数据头部,将']'视作数据结束尾部

TCP特点总结

TCP是一个可靠的,面向连接,流式服务的协议

如何保证可靠:

  1. 应答确认(客户端发送消息,服务器回复ack,客户端收到后再回复ack)

  1. 超时重传(客户端发送消息后,等待服务器回复ack,但一段时间没有收到ack时,会再重发数据,直到收到ack)

  1. 乱序重排(客户端多次发送消息,有的先被接收有的后被接收到,但是在TCP报文中每次数据有序号,它会根据序号重排数据再放入接收缓冲区)

netstat查看TCP连接状态

指令netstat -natp

可以看到TCP连接的ip,端口port,状态等

UDP编程

编程流程:

代码实现

服务器端:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    assert(sockfd != -1);
    struct sockaddr_in saddr,caddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));

    while(1)
    {
        char buff[128] = {0};
        int len = sizeof(caddr);
        recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
        printf("ip = %s port = %d buff = %s\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buff);
        sendto(sockfd,"OK",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
    }
}   

客户端:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    assert(sockfd != -1);
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    while(1)
    {
        printf("input:\n");
        char buff[128] = {0};
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3) == 0)
        {
            break;
        }
        sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
        memset(&buff,0,128);
        int len = sizeof(saddr);
        recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
        printf("buff:%s\n",buff);
    }
    close(sockfd);
    exit(0);
}

结果演示:

UDP数据报服务

并不会像TCP出现粘包问题

UDP特点总结

UDP是一个无连接,不可靠,数据报服务的协议

TCP/UDP各自使用场景

当我们需要传文件时,当一个字节丢失都会导致文件打不开,因此我们选择TCP协议

当我们需要实时通讯时,这类如微信语音聊天,或视频聊天的应用,发送方和接收方需要实时交互,也就是不允许较长延迟,即便有几句话因为网络堵塞没听清也不用等待这部分重新发送。因此选用UDP协议

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逐梦的白鹰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值