Tcp ip 网络编程入门(一)

参考:socket是什么?套接字是什么?;Unix网络编程;socket文件描述符
文章中还有。。。。就不列出来了。
本文是对网上博客内容的一些摘抄与总结,谢谢各位的文章供我学习入门,侵删!

文章目录

        • 1.关于socket
        • 2.核心流程
          • 2.1.创建套接字socket()
          • 2.2.绑定地址bind()
            • 2.21.大小端字节序/网络、主机字节序
          • 2.4.系统监听 listen()
          • 2.5.主动连接connect()
          • 2.6.接受连接accept()
          • 2.7.写读write and read 函数
          • 2.8. 关闭连接close()
        • 3.关于服务器端两个socket的解释

1.关于socket

  socket 原意为“插座”,可理解为电器插入插座便通上电,在网络编程中,socket被翻译为套接字,可理解为计算机通过它可以连接上因特网。
  在Unix/linux中我们知道,万物皆为文件,具体地说,不同种类的类型都被抽象为文件,例如:普通文件,字符设备,块设备,套接字,进程等等。当一个文件被进程打开时,系统会为其创建一个文件描述符fd,这个fd是个整数,这时,文件的路径就成为了寻址系统,文件描述符则成为了字节流的接口。例如:用0表示标准输入文件,其对应的硬件设备为键盘;用1表示标准输出文件,其对应的硬件设备为显示器。
  UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
  文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个“桥梁”的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回的socket描述符就可以了。
   相对于普通文件这类真实存在于文件系统中的文件,tcp socket、unix domain socket等这些存在于内存中的特殊文件在被进程打开的时候,也会创建文件描述符。所以"一切皆文件"更准确的描述应该是"一切皆文件描述符"

2.核心流程

Tcp ip 网络编程入门(一)_第1张图片

2.1.创建套接字socket()

  如上所述,网络连接也是一个文件,它也有文件描述符,可以通过socket()函数创建一个网络连接,其返回值就是文件描述符,有了它,我们就可以用普通的文件操作函数来传输数据了,例如用 read() 读取从远程计算机传来的数据,用 write() 向远程计算机写入数据。
  用于创建一个新的socket,用于客户端和服务端。成功返回一个文件描述符,失败返回-1。(linux中不记得函数形式可用man socket 查看参数及头文件)

int socket (int domain, int type, int protocol);
  • domain:协议簇,可理解为协议的种类,比如网际协议tcp/ip等。
  • type:因为不同的协议提供了不同的数据传输方式,常见的有面向连接的流式传输模式(SOCK_STREAM),顺序,可靠,双向;数据报(SOCK_DGRAM),定长,不可靠。
  • protocol:这里才是具体的协议类型,如tcp,udp。
2.2.绑定地址bind()

  其主要作用是将由socket函数创建的文件描述符和一个本地地址结构体绑定起来。通俗点说,即给新买的手机插上电话卡。成功返回0,失败返回-1,其函数原型为:

int bind (int socketfd, const struct sockaddr *my_addr, socklen_t addrlen)

第一个参数为调用socket函数返回的文件描述符。
  其中,第二个参数即为我们想要绑定的地址,可看到其为一个结构体指针,指向sockaddr这结构体。这个sockaddr为通用的套接字地址,其类型定义为:
Tcp ip 网络编程入门(一)_第2张图片
上述结构体在sa_data中包含了ip,port等信息,考虑到系统的兼容性,一般采用另外一个结构体(struct sockaddr_in)来代替,这个结构体描述了internet环境下的地址形式(可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体):
Tcp ip 网络编程入门(一)_第3张图片
注意到,上述结构体中嵌套了一个结构体
Tcp ip 网络编程入门(一)_第4张图片
这点在具体程序实现中会碰到。
  因为我们在tcpip中所需的地址及端口信息都为internet环境下的,所以先需要sockaddr_in存放ip地址和端口信息,后面在调用bind函数时将一个sockaddr_in{}类型的对象强制转为sockaddr{}类型,再赋值给bind的第二个参数。这两个函数的区别,具体可参考:sockaddr和sockaddr_in详解
struct sockaddr与struct sockaddr_in的区别和联系

结构体成员分析
sin_family:协议簇
sin_port:16位端口号
sin_addr:32位地址信息,以网络字节序保存
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。用memset函数写入0进行初始化即可。
void *memset(void *s,int c,size_t n)
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。linux可用man memset查看参数及头文件。

下面说明以下在这个过程中用到的具体的函数
Tcp ip 网络编程入门(一)_第5张图片 注意:port为unsigned short 16位,addr为 unsigned long 32位,据此选择对应的函数即可。

2.21.大小端字节序/网络、主机字节序

具体可参考文章:网络字节序和主机字节序,网络字节序和主机字节序
网络字节序和主机字节序详解!!!
  不同的机器有不同的字节序类型。考虑一个16位整数,由2个字节组成,内存中存储这个整数的顺序有两种,一种是将低序字节存储在起始地址,称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,称为大端(big-endian)字节序。
这两种字节序没有统一的标准,两种格式都有机器在使用,比如,Inter x86、ARM核采用的是小端模式,Power PC、MIPS UNIX和HP-PA UNIX采用大端模式。
  在数据传输过程中,一定有一个标准化的过程,例如安卓手机充电器接口。也就是说,主机a到主机b的通信,一定是服从:
在这里插入图片描述
网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用大端排序方式
主机字节序:特定主机内的内存的数据处理方式。
对应过来,就有:
在这里插入图片描述

第三个参数addrlen是指第二个参数在sockaddr{}类型下的实际长度,可用sizeof函数。

2.4.系统监听 listen()

一般在基于流式套接字的服务中会有这一步?,成功返回0,失败返回-1,函数原型为;

int listen(int listenfd, int backlog)

该函数的主要作用是将sockfd变为被动的连接的监听套接字
第一个参数:一般来说,socket函数可以创建一个套接字。默认情况,内核会认为socket函数创建的套接字是主动套接字(active socket),它存在于一个连接的客户端。而服务器调用listen函数告诉内核,该套接字是被服务器而不是客户端使用的,即listen函数将一个主动套接字转化为监听套接字(以 listenfd 表示)。监听套接字可以接受来自客户端的连接请求。关于主动套接字和被动套接字的区别:监听套接字与已连接套接字
第二个参数:backlog指明那些已经经过了TCP三次握手的处于established状态的连接项在被系统调度前的最大排队等候数。换句话说,典型的服务器程序可以同时服务于多个客户端,服务器端调用listen函数来声明listenfd处于监听状态,并且最多允许有backlog个tcp连接。

2.5.主动连接connect()

  该函数用于客户端主动向服务器发起连接,成功返回0,失败返回-1.函数原型为:

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

参数情况与前面类似,sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是指向一个套接字地址结构的指针和该结构的大小。
对于TCP套接字,在调用该函数时会激发tcp三次握手的过程。

2.6.接受连接accept()

  其作用是返回一个新的套接字的文件描述符来和客户端通信,serv_addr保存了客户端的IP地址和端口号,而 listenfd 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。成功返回0,失败返回-1,函数原型为:

int accept(int listenfd, struct sockaddr *addr,int *addrlen)

当accept被调用时,服务器程序会一直阻塞,直到有一个客户端发起连接。accept成功时,返回最后的服务器端的文件描述符,失败返回-1。

2.7.写读write and read 函数

可通过man read 查看函数参数和头文件。其函数原型如下:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

  收发数据的套接字内部有缓冲(buffer), 简言之就是字节数组. 通过套接字传输的数据将保存到该数组。因此, 我们 read、write其实是读取缓冲区的内容。
  这两个函数分别由fd指定的socket套接口发送(接收)count字节的数据,然后存在buf缓冲区里面。返回值为实际发送成功的字节数。

2.8. 关闭连接close()

调用该函数时对于TCP编程而言会触发一个FIN报文导致连接关闭。

3.关于服务器端两个socket的解释

可参考文章:为什么有监听socket和连接socket,为什么产生两个socket
从5中的描述上可以看出,accpet生成一个新的socket连接,返回该socket的文件描述符。对服务端来说,有两个socket,一个是用于监听的socket,还有一个就是客户端连接成功后,由accept函数创建的用于与客户端收发报文的socket。我觉得上面的文章总结的很到位:职责分工, 分层协作, 提高服务端性能
基于上面的描述,我们就可以实现一个基本的tcp的客户端与服务端通信的代码。(新手注意不要在if语句后面加分号。可用printf函数或者gdb一步一步调试代码)
服务端:

#include
#include 
#include 
#include
#include
#include
#include
#include 
#include 

int main()
{
    int listenfd;
    if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }
    // printf("socket creat success\n");
    struct sockaddr_in servaddr;  //通过 man 7 ip 查看,按住键盘下,查看隐藏内容;
    memset(&servaddr, 0, sizeof(servaddr));//函数用法上面解释过
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(bind(listenfd,(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("bind");
        return -1;
    }
    // printf("bind success\n");
    if(listen(listenfd,5) < 0)
    {
        perror("listen");
        return -1;
    }
    // printf("listen success\n");
//定义对方的地址
    struct sockaddr_in peeraddr;
    socklen_t peerlen  = sizeof(peeraddr);
    int conn;

    if((conn=accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen))<0)
    {
        perror("accept");
        return -1;
    }
    char recvbuf[1024];
    while(1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        //printf("%d",ret);
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
    close(listenfd);
    close(conn);
    return 0;
}

客户端:

#include
#include 
#include 
#include
#include
#include
#include
#include 
#include 

int main()
{
    int sock;
    if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
    {
        perror("socket");
        return -1;
    }
    struct sockaddr_in servaddr;  //通过 man 7 ip 查看,按住键盘下,查看隐藏内容;
    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    // servaddr.sin_addr = hton1(INADDR_ANY);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if(connect(sock,(struct sockaddr*)&servaddr, sizeof(servaddr))<0)
    {
        perror("connect");
        return -1;
    }
    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));
        fputs(recvbuf,stdout);
    }
    close(sock);
    return 0;
}

makefile文件

all: serve client

serve:serve.c
    gcc -o serve serve.c
client:client.c
    gcc -o client client.c

你可能感兴趣的:(c/c++,网络通信)