基于Linux C的TCP socket编程笔记

一、基本概念

1、socket套接字

对于这个概念我想到了几个问题:socket是什么?是对什么数据结构被操作?
一个socket描述符代表两个地址对 “本地ip:port” 和 “远程ip:port”
socket为内核对象,由操作系统内核来维护其缓冲区,引用计数,并且可以在多个进程中使用。
在使用socket编程时,在网络通信以前首先要建立连接,而连接是通过对socket的一些操作来完成的。建立连接的过程可以分为以下几步:

1) 建立socket套接字

使用socket建立套接字的时候,实际上是建立了一个数据结构。这个数据结构主要的信息是指定了连接的种类和使用的协议,此外还有一些关于连接队列操作的结构字段。
当使用socket函数以后,如果成功的话会返回一个int型的描述符,它指向前面那个被维护在内核里的socket数据结构。任何操作都是通过这个描述符而作用到那个数据结构上的。这就像是在建立一个文件后得到一个文件描述符一样,对文件的操作都是通过文件描述符来进行的,而不是直接作用到inode数据结构上。之所以用文件描述符举例,是因为socket数据结构也是和inode数据结构密切相关,它不是独立存在于内核中的,而是位于一个VFS inode结构中。所以,有一些比较抽象的特性,可以用文件操作来不恰当的进行类比以加深理解。 当建立了这个套接字以后,可以获得一个像文件描述符那样的套接字描述符。就像对文件进行操作那样,可以通过向套接字里面写数据将数据传送到指定的地方,这个地方可以是远端的主机,也可以是本地的主机,还可以用socket机制来IPC(进程间通信)。

2)给套接字赋地址

依照建立套接字的目的不同,赋予套接字地址的方式有两种:服务器端使用bind,客户端使用connetc。

bind:

使用IP, port就可以区分一个TCP/IP连接(这个连接指的是一个连接通道,如果要区分特定的主机间的连接,还需要第三个属性 hostname)。可以使用bind函数来为一个使用在服务器端例程中的套接字赋予通信的地址和端口。
在这里通信的IP地址和端口合起来构成了一个socket地址,而指定一个socket使用特定的IP和port组合来进行通行的过程就是赋予这个socket一个地址。要赋予socket地址,就得使用一个数据结构来指明特定的socket地址,这个数据结构就是struct sockaddr。bind函数的作用就是将这个特定的标注有socket地址信息的数据结构和socket套接字联系起来,即赋予这个套接字一个地址。
一个特定的socket的地址的生命期是bind成功以后到连接断开前。你可以建立一个socket数据结构和socket地址的数据结构,但是在没有bind以前他们两个是没有关系的,在bind以后他们两个才有了关系。这种关系一直维持到连接的结束,当一个连接结束时,socket数据结构和socket地址的数据结构还都存在,但是他们两个已经没有关系了。如果要是用这个套接字在socket地址上重新进行连接时,需重新bind他们两个。
bind指定的IP通常是本地IP(一般不特别指定,而使用INADDR_ANY来声明),而最主要的作用是指定端口。在服务器端的socket进行了bind以后就是用listen来在这个socket地址上准备进行连接。

connect:

对于客户端来说,是不会使用bind的(并不是不能用,但没什么意义),他们会通过connet函数来建立socket和socket地址之间的关系。其中的socket地址是它想要连接的服务器端的socket地址。在connect建立socket和socket地址两者关系的同时,它也在尝试着建立远端的连接。

3) 建立socket连接。

对于准备建立一个连接,服务器端要两个步骤:bind, listen;客户端一个步骤:connect。如果服务器端accept一个connect,而客户端得到了这个accept的确认,则一个连接就建立了。

2、网络字节序

内存中的多字节数据都有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,同样,网络数据流也有大小端之分。
网络数据流的地址规定:先发出的数据时低地址,后发出的数据是高地址。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。

由于两端的两个主机的大小端不一定相同,因此为了使这些网络数据具有更强的可移植性,使相同的代码在大端和小端主机上都能正常运行,我们可以调用以下库函数进行网络字节序和主机字节序的相关转换:

//主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong);//将32长整数从主机字节序转换为网络字节序,
                                  //如果主机字节序是小端,则函数会做相应大小
                                  //端转换后返回;如果主机字节序是大端,则函
                                  //数不做转换,将参数原封不动返回。
uint16_t htons(uint16_t hostshort);

//网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

二、TCP网络通信过程

1、三次握手过程

这里写图片描述

2、通讯流程

这里写图片描述

服务器:

首先调用socket()创建一个套接字用来通讯,其次调用bind()进行绑定这个文件描述符,并调用listen()用来监听端口是否有客户端请求来,如果有,就调用accept()进行连接,否则就继续阻塞式等待直到有客户端连接上来。连接建立后就可以进行通信了。

客户端:

调用socket()分配一个用来通讯的端口,接着就调用connect()发出SYN请求并处于阻塞等待服务器应答状态,服务器应答一个SYN-ACK分段,客户端收到后从connect()返回,同时应答一个ACK分段,服务器收到后从accept()返回,连接建立成功。客户端一般不调用bind()来绑定一个端口号,并不是不允许bind(),服务器也不是必须要bind()。

当客户端没有自己进行bind时,系统随机分配给客户端一个端口号,并且在分配的时候,操作系统会做到不与现有的端口号发生冲突。但如果自己进行bind,客户端程序就很容易出现问题,假设在一个PC机上开启多个客户端进程,如果是用户自己绑定了端口号,必然会造成端口冲突,影响通信。


三、代码实现

1、函数介绍

int socket(int domain,int type,int protocol);
//domain:该参数一般被设置为AF_INET,表示使用的是IPv4地址。还有更多选项可以利用man查看该函数
//type:该参数也有很多选项,例如SOCK_STREAM表示面向流的传输协议,SOCK_DGRAM表示数据报,我们这里实现的是TCP,因此选用SOCK_STREAM,如果实现UDP可选SOCK_DGRAM
//protocol:协议类型,一般使用默认,设置为0

该函数用于打开一个网络通讯接口,出错则返回-1,成功返回一个socket(文件描述符),应用进程就可以像读写文件一样调用read/write在网络上收发数据。

int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//sockfd:服务器打开的sock
//后两个参数可以参考第四部分的介绍

服务器所监听的网络地址和端口号一般是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind来绑定一个固定的网络地址和端口号。bind成功返回0,出错返回-1。
bind()的作用:将参数sockfd和addr绑定在一起,是sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。

 int listen(int sockfd,int backlog);
//sockfd的含义与bind中的相同。
//backlog参数解释为内核为次套接口排队的最大数量,这个大小一般为5~10,不宜太大(是为了防止SYN攻击)

该函数仅被服务器端使用,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//addrlen是一个传入传出型参数,传入的是调用者的缓冲区cliaddr的长度,以避免缓冲区溢出问题;传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。

典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用accept()返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有accept的客户端就处于连接等待状态。
三次握手完成后,服务器调用accept()接收连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

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

这个函数只需要有客户端程序来调用,调用该函数后表明连接服务器,这里的参数都是对方的地址。connect()成功返回0,出错返回-1。

关于通用套接字地址结构struct sockaddr:
这个数据结构是通用的,由于使用的通讯协议族是不确定的,在传参的时候会将特定的协议族地址数据结构指针(struct sockaddr*)强制转为通用的地址数据结构指针(struct sockaddr_in*),然后由内核根据传入的第一个数据结构的第一个成员(sockaddr_family)来确定具体的结构类型。这也确保了不同的协议族地址数据结构可以使用同一个原型的函数。
套接字数据结构本身并不参与通信,仅在主机上使用。

2、服务器代码

#include <stdio.h>
#include <stdlib.h>        //标准库函数和宏
#include <errno.h>         //错误号定义和错误处理
#include <string.h>
#include <sys/socket.h>    //socket函数及数据结构
#include <sys/types.h>     //数据类型定义
#include <netinet/in.h>    //sockaddr_in结构定义
#include <arpa/inet.h>     //IP地址转换函数
#inclued <unistd.h>        //POSIX操作系统API

#define PORT 1500       
#define BACKLOG 1       

int main()
{
    int sockfd, connectfd, addr_len;    //socket文件操作符
    struct sockaddr_in server;          //套接字地址数据结构
    struct sockaddr_in client;          

    sockfd = socket(AF_INET, SOCK_STREAM, 0);   //创建套接字(数据结构)
    if(-1 == sockfd)
    {
        perror("sockfd failed\n");
        exit(1);
    }

   //设置套接字地址数据结构
    bzero(&server, sizeof(server));  
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = htonl(INADDR_ANY);

    //绑定套接字数据结构(内核空间)和套接字地址数据结构(用户空间)
    if(-1 == bind(sockfd, (struct sockaddr*)&server,sizeof(struct sockaddr)))
    {
        perror("bind failed\n");
        exit(0);
    }

    //监听
    if(-1 == listen(sockfd, BACKLOG))
    {
        perror("liseten failed\n");
        exit(1);
    }

    //接收客户端请求并提供服务
    addr_len = sizeof(struct sockaddr_in);
    connectfd = accept(sockfd, (struct sockaddr*)&client,&addr_len );

    if(-1 == connectfd)
    {
        perror("accept failed\n");
        exit(1);
    }

    printf("connect success\n");
    send(connectfd, "Welcome to my server!",22,0);

   // 关闭套接字
    close(connectfd);
    close(sockfd);

    return 0;
}

3、客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>

#define PORT 1500
#define IP "xxx.xxx.xxx.xxx" //更改为server的IP地址
#define MAXSIZE 100

int main()
{
    int sockfd, num;
    char buf[MAXSIZE];
    struct sockaddr_in server;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket failed\n");
        exit(0);
    }

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = inet_addr(IP);

    if(-1 == connect(sockfd, (struct sockaddr*)&server, sizeof(struct sockaddr)))
    {
        perror("connect failed\n");
        exit(-1);
    }

    printf("succsessful connection\n");

    num = recv(sockfd, buf, MAXSIZE, 0);
    printf("count: %d, recv: %s \n", num, buf)
    close(sockfd);

    return 0;
}

四、参考

从问题看本质:socket到底是什么?
socket编程之实现一个简单的TCP通信
Socket编程的头文件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章