一、预备知识

1.什么是ip、port(端口)
在这里插入图片描述

2.如何查看本机ip

ifconfig

3.如何查看tcp 端口号是否被使用和查看缓冲区数据量

netstat -ntlp

二、socket套接字

1.概念
概念一:API是什么?
大多数操作系统使用系统调用的机制在应用程序和操作系统之间传递控制权。

系统调用接口实际上就是应用进程的控制权和操作系统的控制权进行转换的一个接口。由于应用程序在使用系统调用之前要编写一些程序,特別是需要 设置系统调用中的许多参数,因此这种系统调用接口又称为应用编程接口API。
在这里插入图片描述
概念二:套接字作为应用进程运输层协议之间的接口。
在这里插入图片描述

(1)socket 地址API
socket还有一个概念就是(指端口号拼接到IP地址),我们在这里称为socket地址。

我们都知道,TCP把连接作为最基本的抽象,TCP 的许多特性都与TCP是面向连接的这个基本特性有关。

每一条TCP连接有两个端点。那么,TCP连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是运输层的协议端口。TCP连接的端点叫做套接字(socket)或插口。根据RFC 793的定义:端口号拼接到(contatenated with)IP地址即构成了套接字。因此套接字的表示方法是在点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。例如,若IP地址是192.3.4.5而端口号是80,那么得到的套接字就是(192.3.4.5:80)。总之,我们有

套接字socket =(IP地址:端口号)

每一条TCP连接唯一地被通信两端的两个端点(即两个套接字)所确定。即:

TCP连接::= { socket,, socket2} = {(IP1:port), (IP2: portz)}

这里IP,和IP2分别是两个端点主机的IP地址,而 port和 portz分别是两个端点主机中的端口号。TCP连接的两个套接字就是socket,和 sockety。

总之,TCP连接就是由协议软件所提供的一种抽象。虽然有时为了方便,我们也可以说,在-一个应用进程和另一个应用进程之间建立了一条TCP连接,**但一定要记住:TCP连接的端点是套接字,即(IP地址:端口号)。**也还应记住:同-个IP地址可以有多个不同的TCP连接,而同一个端口号也可以出现在多个不同的TCP连接中。

(2)socket 基础API(创建、命名。监听、接受连接、发起连接socket)
1.主机字节序和网络字节序

现代CPU的累加器一次都能装载(至少)4字节,(这里考虑32位机,下同),即一个整数。那么这4字节在内存中排列的顺序将影响他被累加器装载成整数的值。这就是字节序问题。
在这里插入图片描述

现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
大端字节序也称为网络字节序,他给所有接受数据的主机提供了一个正确解释收到的格式化数据的保证。

2.socket地址
2.1通用socket地址
在这里插入图片描述
2.2专用socket地址
通用结构体显然不好用,比如设置与获取ip地址个端口号就需要执行繁琐的位操作,所以提供专门的SOCKET地址结构体。

在这里插入图片描述

所有专用socket地址(以及socketaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程使用的地址参数的类型都是sockaddr 。

 saddr.sin_family = AF_INET;//地址族
    saddr.sin_port = htons(6000);//端口号:需要网络字节序表示。 1024 知名端口 ,4096 保留的端口 , 大于4096 临时端口
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

3.Ip地址转换函数

在这里插入图片描述
4.创建socket

socket 是一个可读可写可控制可关闭的文件描述符。

在这里插入图片描述
在这里插入图片描述


    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接字//int socket(int domain,int type,int protocol);
    if ( sockfd == -1 )//domain  参数告诉系统使用哪个底层协议族,type指定服务类型(流服务SOCK_STREAM TCP协议,传输层UDP 协议:SOCK_DGRAM)
    {                         //Socket 第三个参数是再选择一个具体的协议。基本设置为0  ,表示使用默认协议。
        exit(1);
    }

//AF_INET 地址族可以和PF_INET协议族混用 (因为都定义在一个头文件中)

//创建监听套接字//int socket(int domain,int type,int protocol);
//domain 参数告诉系统使用哪个底层协议族;
type指定服务类型(流服务SOCK_STREAM TCP协议,传输层UDP 协议SOCK_DGRAM);
//Socket 第三个参数是再选择一个具体的协议。基本设置为0 ,表示使用默认协议。

5.命名(bind)socket

当套接字被创建后,它的端口号和IP地址都是空的,因此应用进程要调用bind(绑定)来指明套接字的本地地址(本地端口号和本地IP地址)。在服务器端调用bind时就是把熟知端口号和本地IP地址填写到已创建的套接字中。这就叫做把本地地址绑定到套接字。在客户端也可以不调用bind,这时由操作系统内核自动分配一个动态端口号(通信结束后由系统收回)。
在这里插入图片描述
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。
bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCES和EADDRINUSE。

这里的sockaddr*my_addr 是通用地址,如果程序员使用的是专用的地址,那么在使用BIND函数时应注意把专用地址转换为通用地址。

6.listen socket(创建监听队列)
服务器在调用bind后,还必须调用listen(收听)把套接字设置为被动方式,以便随时接受客户的服务请求。UDP服务器由于只提供无连接服务,不使用listen系统调用。

在这里插入图片描述

7.接受socket
服务器紧接着就调用accept(接受),以便把远地客户进程发来的连接请求提取出来。系统调用accept 的-一个变量就是要指明从哪一个套接字发起的连接。
在这里插入图片描述

sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回–个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1并设置errno。

7.1 一个服务器处理多个连接:并发方式???
8.客户端,connect (建立连接)
在这里插入图片描述

sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。
connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的crrno是ECONNREFUSED和ETIMEDOUT。

9.send(TCP数据写)
在这里插入图片描述
send往sockfd 上写入数据,buf和l len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errnoo。
10.recv(TCP数据读)

recv读取sockfd上的数据,buf和 lcn参数分别指定读缓冲区的位置和大小,flags参数的含义见后文,通常设置为О即可。recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度1en。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno.

11.释放(释放套接字和连接)

系统调用的使用顺序
在这里插入图片描述

三、TCP非并发编程代码

serve.c代码:

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

int main()
{ //AF_INET 地址族可以和PF_INET协议族混用 (因为都定义在一个头文件中)
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接字//int socket(int domain,int type,int protocol);
    if ( sockfd == -1 )//domain  参数告诉系统使用哪个底层协议族,type指定服务类型(流服务SOCK_STREAM TCP协议,传输层UDP 协议:SOCK_DGRAM)
    {                         //Socket 第三个参数是再选择一个具体的协议。基本设置为0  ,表示使用默认协议。
        exit(1);
    }

    struct sockaddr_in saddr,caddr;//ipv4
    memset(&saddr,0,sizeof(saddr));//初始化  置为0
    saddr.sin_family = AF_INET;//地址族
    saddr.sin_port = htons(6000);//端口号:需要网络字节序表示。 1024 知名端口 ,4096 保留的端口 , 大于4096 临时端口
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//函数inet_addr()将点分十进制ip地址转为2进制ip地址
    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if ( res == -1 )
    {
        close(sockfd);
        exit(1);
    }
    res = listen(sockfd,5);
    if ( res == -1 )
    {
        close(sockfd);
        exit(1);
    }

    while( 1 )
    {
        int len = sizeof(caddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
        if ( c < 0 )//c 连接套接字
        {
            continue;
        }
        printf("accept:%d\n",c);

        while( 1 )
        {
            char buff[128] = {0};
            int n = recv(c,buff,127,0);//read(c,buff,127)
            if( n <= 0 )
            {
                break;
            } 
            printf("buff(%d)=%s\n",c,buff);
            send(c,"ok",2,0);
        }
        close(c);
    }
}

cli.c代码:

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

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//TCP的两端各一个套接字,所以两端都要创建套接字。
    if ( sockfd == -1 )
    {
        exit(1);
    }

    struct sockaddr_in saddr;//注意这里的结构体存放的地址信息是主机的。
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);// 1024 知名端口 ,4096 保留的端口 , 大于4096 临时端口
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//一旦连接成功,返回的res(一个socket)唯一地标识这个连接,客户端就可以读写socket与服务器通讯。
    if ( res == -1 )
    {
        printf("connect failed\n");
        close(sockfd);
        exit(1);
    }

    while( 1 )
    {
        printf("input\n");
        char buff[128] = {0};
        fgets(buff,128,stdin);

        if ( strncmp(buff,"end",3) == 0 )
        {
            break;
        }

        send(sockfd,buff,strlen(buff)-1,0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);
        printf("buff=%s\n",buff);//"ok"
    }
    close(sockfd);//
    exit(0);
}

int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//一旦连接成功,返回的res(一个socket)唯一地标识这个连接,客户端就可以读写socket与服务器通讯。

四、TCP并发编程

以上代码可以发现service.c代码只能处理一个客户端的消息。
怎么改才能实现并发编程呢?

我们在这里用新建线程去跟踪每个客户端。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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)

        {
            break;
        }
        printf("recv(%d") = % s\n",c,buff);
            send(c, "ok", 2, 0);

    }
    printf("client close\n");
    close(c);
}
int main()
{ //AF_INET 地址族可以和PF_INET协议族混用 (因为都定义在一个头文件中)
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接字//int socket(int domain,int type,int protocol);
    if ( sockfd == -1 )//domain  参数告诉系统使用哪个底层协议族,type指定服务类型(流服务SOCK_STREAM TCP协议,传输层UDP 协议:SOCK_DGRAM)
    {                         //Socket 第三个参数是再选择一个具体的协议。基本设置为0  ,表示使用默认协议。
        exit(1);
    }

    struct sockaddr_in saddr,caddr;//caddr是放客户端地址。
    memset(&saddr,0,sizeof(saddr));//初始化  置为0
    saddr.sin_family = AF_INET;//地址族
    saddr.sin_port = htons(6000);//端口号:需要网络字节序表示。 1024 知名端口 ,4096 保留的端口 , 大于4096 临时端口
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//函数inet_addr()将点分十进制ip地址转为2进制ip地址
    int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//地址弄完要使用bind函数将socket和sock地址绑定
    if ( res == -1 )
    {
        close(sockfd);
        exit(1);
    }
    res = listen(sockfd,5);
    if ( res == -1 )
    {
        close(sockfd);
        exit(1);
    }

    while( 1 )
    {
        int len = sizeof(caddr);
        int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//这里接收到的客户端的ip端口写到caddr中。接收成功返回一个新的SOcket,这个socket 唯一标识了这个被接收的连接,服务器端通过它读写与客户端通讯。
        if ( c < 0 )//c 连接套接字
        {
            continue;
        }
        printf("accept:%d\n",c);

        ptherad_t id;
        pthread_cread(&id, NULL, work_thread, (void*)c);//每接受一个客户端的连接后就创建一个新的线程去recv().c转成无类型指针。
    }
}

int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//这里接收到的客户端的ip端口写到caddr中。接收成功返回一个新的SOcket,这个socket 唯一标识了这个被接收的连接,服务器端通过它读写与客户端通讯。

思考:为什么会出现下图这种情况(buff一次打印了5个ok??)
在这里插入图片描述

在这里插入图片描述

send ()和recv() 不是收发一一对应的关系(并不是收一次ok就发一次ok),缓冲区中的内容不一定是一次全部发送,也可能分次发送。有时也收到了input 阻塞的影响,对接收缓冲区的数据也和想象的不一致。

OSI模型、TCP/IP协议

OSI模型(分析时多用到)
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层

TCP/IP协议(大多数用的是这个)
应用层
传输层(进程间通讯)
网络层(可跨越节点的传输)
数据链路层(相邻节点数据的传输)

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐