TCP和UDP通信实例实现

首先实现一个简单的服务器端和客户端通信的小例子.

TCP方式

服务器端调用步骤:

  1. 调用socket函数创建套接字
  2. 调用bind函数分配IP地址和端口号
  3. 调用listen函数将套接字转为可接收连接状态
  4. 调用accept函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。
  5. 使用write函数用于向客户端传输数据。

客户端:

  1. 调用客户端创建套接字

TCP相关API

创建socket

函数原型如下:

1
int socket(int domain, int type, int protocol)
  • socket函数成功时返回一个文件描述符(见后续其他相关知识),失败则返回-1.

  • domain: 套接字中使用的协议族信息

    • PF_INET表示IPV4互联网协议族
  • type: 套接字数据传输类型信息

    • SOCK_STREAM表示面向连接的套接字

    • SOCK_DGRAM表示面向消息的套接字.

    • 创建TCP套接字和UDP套接字: 

      1
      2
      tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
      udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
  • protocol: 计算机间通信中使用的协议信息

  • 描述socket

    • 接下来代码描述socket文件,包括socket文件的大小,接受的内容等
    1
    2
    3
    4
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    • sin_family: 地址族

      每种协议族适用的地址族不同.IPV4使用4字节地址族.IPV6使用16字节地址族.

      • AF_INET IPV4网络协议中使用的地址族
      • AF_INET6 IPV6网络协议中使用的地址族
    • sin_port: 该成员保存16位端口号,它以网络字节序保存

    • sin_addr: 该成员保存32位IP地址信息,且也以网络字节序保存.

地址绑定

给套接字分配地址信息(IP地址和端口号)

1
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);

成功时返回0,失败时返回-1.
sockfd: 要分配地址(IP地址和端口号)的套接字文件描述符
myaddr: 存有地址信息的结构体变量地址值
addrlen: 第二个结构体变量的长度

监听

  • 绑定了地址信息后,只需要最后一步接收来自客户的连接请求了.这就是让套接字变成监听状态,之后,只要客户端向服务器发送请求,服务器就可以接收到了.
    通过listen函数进入等待连接请求状态.只有调用了listen函数,客户端才能进入可发出连接请求的状态.换言之,这时客户端才能调用connect函数(若提前调用将发生错误).

    1
    int listen(int sock, int backlog);

    成功时返回0,失败时返回-1.
    sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)
    backlog: 连接请求等待队列的长度, 若为5,则队列长度为5,表示最多使5个连接请求进入队列.

  • 等待连接请求状态

    • 服务端处于等待连接请求状态是指,客户端请求连接时,受理连接前一直使请求处于等待状态.
    • 连接请求队列:允许的连接请求的客户端个数 

接受连接

函数原型如下:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sock: 服务器套接字的文件描述符

  • addr: 保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息.

  • addrlen: 第二个参数addr结构体的长度,但是存有长度的变量地址.函数调用完成后,该变量即被填入客户端地址长度.

  • 返回值:accept函数成功时返回文件描述符,失败时返回-1

  • accept函数受理连接请求队列中待处理的客户端连接请求.函数调用成功时,accept函数内部将产生用于数据IO的套接字,并返回其文件描述符.套接字是自动创建的,并自动与发起连接请求的客户端建立连接

  • 一个服务器端socket可以接收很多客户端的连接请求.当客户端发起连接请求时,服务器端需要调用accept函数受理客户端的请求.同时来自不同客户端的连接也会当成不同的socket文件,返回不同的文件描述符.

connect

1
int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
  • 功能:客户端调用connect函数后,发生以下情况之一才会返回(完成函数调用)
    • 服务器接收连接请求
      • 注意:接收连接请求并不意味着服务器调用accept函数,而是指服务器把连接请求信息记录到等待队列
      • 所以,connect函数返回后并不立即进行数据交换
    • 发生断网等异常情况而中断连接请求
  • sock: 客户端套接字文件描述符
  • servaddr: 保存目标服务端地址信息的变量地址值
  • addrlen: 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度

read和write

1
ssize_t read(int fd, void* buf, size_t nbytes);

成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1.
fd: 显示数据接收对象的文件描述符.
buf: 要保存接收数据的缓冲地址值.
nbytes: 要接收数据的最大字节数.

1
ssize_t write(int fd, const void* buf, size_t nbytes);

成功时返回写入的字节数,失败时返回-1.
fd: 显示数据传输对象的文件描述符.
buf: 要保存传输数据的缓冲地址值.
nbytes: 要传输数据的最大字节数.

fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

  • 功能:从给定流 stream 读取数据到 ptr 所指向的数组中
  • ptr – 这是指向带有最小尺寸 size\nmemb* 字节的内存块的指针。
  • size – 这是要读取的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

fgets

函数原型为:char *fgets(char *str, int n, FILE *stream)

  • 功能:从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。fgets会把读到字符写到s指向的字符串里,直到出现下面某种情况:

    • 遇到换行符
    • 已经传输了n-1个字节
    • 到达文件尾

    它会把遇到的换行符也保存到s中.最后在s末尾加上一个表示结尾的空字符\0.一次调用最多只能传输n-1个字符,因为它必须把空字符加到字符串表示结尾.

  • gets函数类似于fgets,但是它从标准输入流中取数据并丢弃遇到的换行符.它在接收字符串的尾部加上一个空字节(一个字符占一个字节).

fgetc

fgetc从文件指针stream指向的文件中读取一个字符,读取一个字节后,光标位置后移一个字节。

fputc

功能:把一个字符写到一个输出文件流中.它返回写入的值,如果失败,则返回EOF.
putc函数的作用相当于fputc,但它可能被实现为一个宏.
putchar相当于putc(c, stdout),但它把单个字符写到标准输出.注意:putchar和getchar都是把字符当做int类型而不是char类型来使用的.这就允许文件尾标识取值-1,这是一个超出字符数字编码范围的值.

fopen

https://blog.csdn.net/hairetz/article/details/4150193

sigaction

1
sigaction(int signo, const sigaction* act, struct sigaction *oldact);
  • 功能:
  • signo:与signal函数相同,传递信号信息
  • act:对应于第一个参数的信号处理函数(信号处理器)信息
  • oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
  • 返回值:成功时返回0,失败时返回-1

UDP方式

服务端:
客户端:

UDP相关API

TCP套接字将会保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,因此每次传输数据都要添加目标地址信息。

sendto

1
2
#include<sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);

功能:
返回值:成功时返回传输的字节数,失败时返回-1。
sock:用于传输数据的UDP套接字文件描述符。
buff:保存待传输数据的缓冲地址值。
nbytes:待传输的数据长度,以字节为单位。
flags:可选项参数,若没有则传递0。
to:存有目标地址信息的sockaddr结构体变量的地址值。
addlen:传递给参数to的地址值结构体变量长度。
这个函数在发送端主机上被调用,而发送数据的UDP套接字只有在发送端主机才有意义。

recvfrom

1
2
#include<sys/socket.h>
ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* from, socklen_t *addrlen);

返回值:成功时返回接收的字节数,失败时返回-1。
sock:用于接收数据的UDP套接字文件描述符。
buff:保存接收数据的缓冲地址值。
nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲区大小。
flags:可选项参数,若没有则传入0。
from:存有发送端地址信息的sockaddr结构体变量的地址值。
addlen:保存参数from的结构体变量长度的变量地址值。

其他相关知识

  • 文件描述符
    • 对linux来说,一切皆文件.套接字和本地的文件都是文件.
    • 文件描述符是系统分配给文件或套接字的整数.是为了方便称呼操作系统分配的文件或套接字.
  • 结构体sockaddr_in

参考文献

[1] TCP/IP网络编程:https://blog.csdn.net/u011675745/article/details/78555250