Linux下的Socket编程(主要包括TCP部分)

Linux下的Socket编程(主要包括TCP部分)

转载麻烦注明原文地址
本文是Linux下基本的Socket编程进行介绍,主要包括以下知识点

  • Linux下的一些小知识(编译常识,文件相关操作基础,进程与线程)
  • TCP网络编程基础(TCP Socket)
  • 基于UDP协议的接受和发送(UDP Socket),以及网络中的大端存储,小端存储将在后一篇文章介绍
  • 1.Linux下一些小知识

这些基础摘自《Linux网络编程(第二版)》,仅是对个人的一些Linux常识补充,读者可选择性忽略,直接进入网络编程环节。

(1).程序编译常识在Linux中,程序采用的是最广泛的是GCC编译,程序从源代码文件到指定的可执行文件从要经历一系列过程,本段将对这段过程做个概述。先看一张图:

编译顺序图.png

由于本文所涉及到的socket编程都是C语言下的Socket编程,故最开始的代码源文件都是.c文件。源文件,目标文件和可执行文件是编译过程中常用到的名词。
源文件通常是指存放可编辑代码的文件。目标文件是指经过编译器的编译生成的CPU可识别的二进制代码,但是目标文件不能用与执行,因为其中的一些函数过程没有相关的指示和说明。可执行文件就是目标文件与相关链接库链接后的文件,是可以执行的。
  预编译过程:预编译过程采用的是" gcc -E 文件名.c"命令的形式将源文件进行预编译,该过程的目的是将程序中引用的头文件包含进源代码中,并对一些宏进行替换,生成后缀名为.i的中间文件。
  编译过程:编译过程将用户可识别的语言翻译成一组处理器可识别的操作码,通常翻译成汇编语言。汇编语言通常和机器操作码是一对一的关系,编译过程生成汇编语言的GGC 选项是-S,生成后缀名为.s的汇编文件。
  汇编过程:汇编过程当然就好理解了,就是讲汇编文件使用GCC选项-C进行汇编,将汇编文件翻译成机器可以识别的机器操作码,也就是后缀名是.obj或者.o目标文件。
  链接过程:所有目标文件必须通过某种方式组合起来才能运行,目标文件中仅解析了文件内部的变量和函数,对于引用的函数和变量还没有解析,这就需要将其他已经编写好的文件引用进来,对没有解析的变量和函数进行解析,通常引用的目标是库,链接完成之后生成文件名为a.out的可执行文件。
  上述小结:上述过程仅仅是对在Linux下使用GCC编译器编译的各个过程分开详细描述,对于单个.C的源文件,直接使用gcc命令加上要变异的C语言源文件,GCC会自动生成文件名为a.out的可执行文件,自动的过程包括了头文件扩展,目标文件编译,以及链接默认的系统库一些列操作,最后生成系统默认的可执行程序a.out。如:gcc hello.c,就会直接编译成可执行文件。
(2).链接库与加载库
  静态链接库:静态库是obj文件的集合,通常静态库以“.a”为后缀,静态库由程序ar生成。静态库的一个优点或者说作用就是可以在不用重新编译程序库代码的情况下进行程序重新链接,这种方法大大节省了编译过程的时间。但是由于现在计算机系统的日益强大,编译的时间已经不是问题;静态库的另一个优势便是可以提供库文件给使用人员,而不用公开源代码。
  动态链接库:动态链接库是程序运行时加载的库,当动态链接库正确安装之后,所有程序都可以使用动态库来运行程序,动态链接库是目标文件的集合,但目标文件在动态链接库中的组织方式是按照特殊的方式形成的。
  静态加载库:动态加载库和一般的动态链接所不同的是,一般动态链接库在程序启动的时候就要寻找动态库,找到库函数;而动态加载库可以用程序的方法来控制什么时候加载。动态加载库主要有函dlopen(),dlerror(),dlsym()和dlclose(),具体使用方式,本文不做过多介绍,网上资源丰富,请读者自行百度。
(3).Linux下文件相关基础
  Linux下文件的内涵:文件系统狭义的概念是一种对存储设备上的数据进行组织和控制的机制,在Linux下(当然包括UNIX),文件的含义比较广泛,文件的概念不仅仅包括通常意义的保存在磁盘上的各种各样的数据,还包括各种各样的数据,如鼠标,键盘,网卡,标准输入输出等。“一切皆文件”
  文件描述符:在Linux下用文件描述符来表示设备文件和普通文件。文件描述符是一个整型的数据,所有对文件的操作都通过文件描述符实现。文件描述符的范围是0~OPEN_MAX。  文件描述符是文件系统中连接用户空间和内核空间的枢纽,当打开一个或者创建一个文件时,内核空间创建相应的结构,并生成一个整形变量传递给用户空间的对应进程,进程用这个文件描述符来对文件操作
  在Linux系统中有3个已经分配的文件描述符,即标准输入、标准输出和标准错误,它们文件描述符的值分别为0、1和2。
  文件相关操作:open(),create(),close(),read(),write(),lseek(),这里就不详细介绍每个函数的用法了,读者请自行百度。知道的请忽略该内容,这部分内容有助于理解后面的TCP Socket内容。

  • 2.TCP网络编程基础(TCP Socket)
    (1).套接字相关基础知识
      套接字地址结构:套接字编程需要指定套接字地址作为参数(套接字不是套接字地址,不要搞混,后面会有介绍),不同的协议族有不同的地址结构,这些地址结构通常以sockaddr_作为开头,并且每一个协议族有一个唯一的后缀,例如对于以太网,其结构名称就是sockaddr_in。
  • 通用套接字数据结构:

sa_family_t sa_family; /协议族/ char sa_data[14]; /协议族数据/ } ```

  • 实际使用的套接字结构:(比如在以太网中)

u8 sin_len /结构struct socket_in 的长度,16/
u8 sin_family /通常为AF_INT 协议族/
u16 sin_port /16为的端口号,网络字节序/
struct in_addr sin_addr /IP地址32位/
char sin_zero[8] /未用/ };

struct in_addr { u32 s_addr /* 32位IP地址,为网络字节序*/ }```

  • 结构sockaddr 和结构 sockaddr_in的关系
    两种结构关系图.png
    由于以上两种结构的大小是完全一致的,所以在进行地址结构设置时,通常的方法是利用结构struct sockaddr_in进行设置,然后强制转化为struct sockaddr类型,因为两个结构大小完全一致,所以这样的转换不会有副作用。
    (2).TCP网络编程流程   总的来说,TCP网络编程有两端,服务端创建一个服务程序,等待客户端用户连接,接收到用户的连接请求之后,根据用户的请求进行处理;客户端则根据目的服务器的地址和端口进行连接,向服务端发送请求,并对服务器的响应进行数据处理。先总体看看两者是如何交互的:
    TCP C&S交互过程.png
    由图可以看到对于服务器来说流程主要分为套接字初始化(socket()),套接字与端口的绑定(bind()),设置服务器的侦听连接(listen()),接受客户端连接(accept()),接收和发送数据(read()、write())并进行数据处理及处理完毕的套接字关闭(close()),而对于客户端来说分为套接字初始化(socket()),连接服务器(connect()),读写网络数据(read()、write())并进行数据处理和最后的套接字关闭(close())过程。所以两者的区别在与客户端在创建了套接字之后不进行地址绑定(不要着急,后面会介绍地址绑定),而是直接连接服务器端。下面详细介绍各个函数:
    (3).TCP网络编程流程函数详解
  • socket() 函数原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

socket()函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回–1。   参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信的协议族,通信的协议族在文件sys/socket.h定义,包含下表所示的值,以太网应该设置PF_INET这个域,在程序设计的过程中会发现有的代码使用了AF_INET这个值,在头文件中AF_INET和PF_INET的值是一致的。


domain的值及其含义.png

  参数type用于设置套接字通信的类型,主要有SOCK_STREAM(流式套接字),SOCK_DGRAM(数据包套接字)。其余类型如下图所示:


type的值及其含义.png
函数socket()并不总是执行成功,有可能会出现错误,错误的原因有很多种,可以通过errno获得。在TCP中可以通过socket(AF_INET,SOCK_STREAM,0)返回一个TCP的套接字文件操作符。
  • bind() 函数原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

bind()函数有三个参数,第一个参数sockfd是用socket()函数创建的文件操作符,第二个参数my_addr是指向一个struct sockaddr参数的指针,sockaddr中包含了地址,端口,以及IP地址的信息,在进行地址绑定的时候,需要先将地址结构中的IP地址,端口,类型等结构struct sockaddr中的域进行设置之后才能绑定,这样进行绑定之后才能将套接字文件描述符与地址等结合在一起。第三个参数addrlen是my_addr结构的长度,可以设置为sizeof(struct sockaddr)最后,bind()函数返回值为0表示绑定成功,返回-1表示失败,同样失败时可以通过查看errno来查看原因。

  • listen()函数原型:
int listen(int sockfd, int backlog);```

函数中的sockfd表示当前监听的套接字的文件描述符,很容易理解。backlog表示在accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端就会返回一个ECONNREFUSED错误。成功运行时,返回值为0,当运行失败时,返回值为-1,并且设置erno值。注:listen函数仅仅对类型为SOCK_STREAM或者SOCK_SEQPACKET的协议有效

  • accept()函数原型:

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

当一个客户端的连接请求到大服务器主机监听的端口时,此时客户端的连接会在队列中等待,直到服务器处理用户请求。函数accept()成功执行之后,会返回一个新的套接字描述符来表示客户端的连接,客户端连接的信息可以通过这个新的描述符来获得,因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示当前服务端正在监听的那个socket,新产生的描述符表示客户端的连接,使用这个新的描述符就可以执行下面要提到的文件传输等工作了。  参数介绍:当accept函数成功执行之后,客户端的信息就可以通过上面的addr来获得了,其中包括了客户端的IP,端口,协议族等内容。第三个参数表示的是第二个参数的长度,同理,也可以使用sizeof(struct sockaddr_in)获得。注意:在accept中,addrlen参数是一个指针而不是一个结构体。函数返回值表示其成功与否,同理,-1表示失败,errno依然可以获得其失败的原因。- 对客户端来说的connect()函数  客户端在建立套接字之后,不需要进行地址绑定就可以直接连接服务器,连接服务器的函数就是connect(),此函数需要指定参数服务器,例如IP地址,端口等。函数原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

在客户端,这里的sockfd便指的是客户端建立套接字返回的文件描述符,第二个参数是一个指向struct sockaddr这种结构体的一个指针,里面包含了客户端需要连接服务器的目的端口,IP地址,以及协议类型等信息,第三个参数表示了第二个参数内容的大小也可以使用sizeof(struct sockaddr)获得。函数返回值表示其成功与否,同理,-1表示失败,errno依然可以获得其失败的原因。

  • 写入数据函数write()
       当服务端在收到一个客户端连接之后,可以通过套接字描述符进行数据写入工作,对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当写入对象为套接字的时候,会调用对应的内核函数。函数返回的大小为成功写入的字节数,写入函数较为简单,这里不做更多介绍。
  • 读取数据函数read()
       与写入数据类似,使用read函数可以从套接字描述符中读取数据,当然,在读取数据之前,必须建立套接字连接,具体函数不做过多介绍。

总结,本文仅仅介绍了本人在学习过程中的一些小知识以及TCP套接字建立简单的流程和其简单的用法,相信读者在阅读之后对TCP Socket的整个过程会有了一个比较好的把握(大神自动忽略哈,嘻嘻)。涉及到TCP相关的其他异常控制,本文还没对其进行讨论,最后附上完整的TCP服务端和客户端代码:https://github.com/OsCinux/TCP_Basic_Socket

推荐阅读更多精彩内容