Linux编程——网络编程(tcp)
tcp协议 (传输控制协议)特征:
1.流式套接字
2.数据连续,顺序发送
3.可靠传输,应答机制(保障数据可靠传输),数据发送丢包后启动自动重传机制
4.有链路(服务器和客户端会先打通一条通路,建立连接,连接server和client,互联网传输节点状态信息可以保存)
5.全双工通信
6.机制复杂
7.双缓冲区,收发互不影响
建立连接——三次握手
握手次数 | 发送方 | 核心内容 [seq:序列号] | 目的 |
---|---|---|---|
第一次 | 客户端 | SYN=1, Seq=X | 发起连接,告知服务器:客户端的起始序列号。 |
第二次 | 服务器 | SYN=1, ACK=1, Seq=Y, Ack=X+1 | 同意连接,告知客户端:服务器的起始序列号,并确认收到了你的第一个包。 |
第三次 | 客户端 | ACK=1, Seq=X+1, Ack=Y+1 | 确认收到了你的包。至此,双方都确认了对方的发送能力和自己的接收能力。 |
最核心、必须发送的内容就是双方的初始序列号和对对方序列号的确认!!!
断开连接——四次挥手
操作流程
listen()函数不会阻塞,用于监听服务器状态,接收请求连接的客户端,并与客户端完成三次握手建立通信连接
read() --- write() 也可以调用 send() --- recv()
客户端/服务器端,任意一端断开,套接字进入半链接状态,只能收不能发
服务器端
<1>sock() 建立流式套接字
头文件:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型: int socket(int domain, int type, int protocol);
功能: 创建一个通信端点(套接字),并返回一个文件描述符。这是网络通信的起点。
参数:
int domain
:协议族。常用AF_INET
(IPv4)或AF_INET6
(IPv6)。int type
:套接字类型。常用SOCK_STREAM
(面向连接的可靠TCP流)或SOCK_DGRAM
(无连接的UDP数据报)。int protocol
:通常设置为0
,表示由系统根据前两个参数自动选择默认协议(如SOCK_STREAM
默认对应IPPROTO_TCP
)。
返回值: 成功 返回一个非负整数的文件描述符
失败 返回-1
示例代码
//创建流式套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if(-1 == listfd){perror("socket fail");return 1;}
<2>bind() 绑定服务器地址和端口
头文件:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>#include <netinet/ip.h>
原型: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 将上一步创建的套接字与一个特定的本地IP地址和端口号绑定起来。服务器必须这么 做,以便客户端能够知道要连接到哪台机器的哪个服务。
参数:
int sockfd
:socket()
函数返回的套接字描述符。const struct sockaddr *addr
:指向一个包含协议家族、IP地址、端口号等信息的结构体的指针。实际使用时,通常填充
struct sockaddr_in
(用于IPv4),然后强制转换为struct sockaddr *
socklen_t addrlen
:第二个参数所指向的结构体的大小(sizeof(struct sockaddr_in)
)
返回值: 成功 返回0;
失败 返回-1,并设置errno
struct sockaddr_in
- 定义IPv4套接字地址
struct in_addr
- 存储IPv4地址
示例代码
struct sockaddr_in address;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));address.sin_family = AF_INET; // IPv4
//127.0.0.1 表示自己的IP地址,本机自己接收
// ser.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_addr.s_addr = INADDR_ANY; // 绑定到本机所有可用的IP地址
address.sin_port = htons(8080); // 绑定到8080端口 (htons用于主机字节序到网络字节序的转换)int ret =bind(listfd,(SA)&ser,sizeof(ser));
if(-1 == ret)
{perror("bind fail");return 1;
}
<3>listen() 使套接字进入监听
头文件:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型: int listen(int sockfd, int backlog);
功能: 将一个已绑定的套接字置于被动监听状态,告诉操作系统该套接字用于接受来自客户端的连接请求。此调用后,套接字从“CLOSED”状态变为“LISTEN”状态。
参数:
int sockfd
:已通过bind()
绑定的套接字描述符。int backlog
:连接请求队列的最大长度。当多个客户端同时发起连接时,已完成三次握手但尚未被服务器accept()
的连接会排在这个队列里。此参数定义了队列的最大值。通常设置为5
、10
或更大,如SOMAXCONN
(系统允许的最大值)
返回值: 成功 返回0
失败 返回-1,并设置errno
示例代码
//监听套接字(建立连接),3-三次握手的排队的客户端个数(同一时刻)
listen(listfd,3);
<4>accept() 接受(建立)连接 【发生三次握手】
头文件:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能: 从 listen() 设置的连接请求队列中取出一个已建立的连接,并为这个连接创建一个新的 套接字。原来的监听套接字继续用于接受其他连接。
//这是一个阻塞调用(默认情况下)。如果队列为空,进程会进入睡眠状态,直到 有新的连接到达。
参数:
int sockfd
:处于监听状态的套接字描述符(server_fd
)。struct sockaddr *addr
:一个指向struct sockaddr
的指针,用于获取客户端的地址信息(IP和端口)。如果不需要,可以设为NULL
。socklen_t *addrlen
:这是一个值-结果参数。调用时,需要将其初始化为addr
指向的缓冲区的大小。函数返回时,它会被设置为实际存放地址信息的长度。
返回值: 成功 返回一个新的套接字描述符(new_socket),这个新套接字是专门用于与这个 特定客户端通信的。
失败 返回-1 ,并设置errno
示例代码
//通信套接字,用于后续通信的通道int conn = accept(listfd,(SA)&cli,&len);if(-1 == conn){perror("accept fail");return 1;}
<5>recv() 接收数据
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 从已连接的套接字(由 accept()
返回的)接收对端发送过来的数据
参数:
int sockfd
:已连接的套接字描述符(new_socket
)。void *buf
:指向接收缓冲区的指针,用于存放接收到的数据。size_t len
:接收缓冲区的最大长度。int flags
:标志位,用于控制接收行为。通常设置为0
(表示阻塞模式,常规操作)
返回值: 成功 返回0(0表示对端已经关闭连接,即收到了FIN包); 返回实际接收到的字节数(>0)
失败 返回-1,并设置errno
<6>send() 发送数据
头文件:#include <sys/types.h>
#include <sys/socket.h>
原型: ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能: 从已连接的套接字(由 accept()
返回的)接收对端发送过来的数据
参数:
int sockfd
:已连接的套接字描述符(new_socket
)。const void *buf
:指向要发送数据的缓冲区的指针。size_t len
:要发送的数据的字节数。int flags
:标志位,通常设置为0
返回值: 成功 返回实际发送出去的字节数
失败 返回-1,并设置errno
<7>close() 关闭套接字和监听套接字
服务器端示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>typedef struct sockaddr *SA;int main(int argc, char **argv)
{//监听套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if(-1 == listfd){perror("socket fail");return 1;}//man 7 ipstruct sockaddr_in ser,cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);//127.0.0.1 表示自己的IP地址,本机自己接收// ser.sin_addr.s_addr = inet_addr("127.0.0.1");ser.sin_addr.s_addr = INADDR_ANY;int ret =bind(listfd,(SA)&ser,sizeof(ser));if(-1 == ret){perror("bind fail");return 1;}//监听套接字(建立连接),3-三次握手的排队的客户端个数(同一时刻)listen(listfd,3);socklen_t len = sizeof(cli);//通信套接字int conn = accept(listfd,(SA)&cli,&len);if(-1 == conn){perror("accept fail");return 1;}time_t tm;while (1){char buf[1024] = {0};int ret = recv(conn,buf,sizeof(buf),0);if(ret <= 0) //客户端断开连接{break;}time(&tm);sprintf(buf,"%s %s",buf,ctime(&tm)); //数据处理send(conn,buf,strlen(buf),0); }close(listfd);close(conn);return 0;
}
客户端
头文件:#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
<1>socket() 创建套接字
<2>connect() 发起连接
原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能: 客户端调用此函数向指定的服务器发起连接请求。它会触发TCP三次握手过程。
参数:
int sockfd
:socket()
函数返回的套接字描述符。const struct sockaddr *addr
:指向一个包含服务器协议家族、IP地址、端口号等信息的结构体的指针。这是连接的目标地址。(结构体指针信息详细见服务器端 bind()函数)实际使用时,填充
struct sockaddr_in
,然后强制转换为struct sockaddr *
。
socklen_t addrlen
:第二个参数所指向的结构体的大小
返回值: 成功 返回0 (表示此时三次握手成功,TCP连接已建立)
失败 返回-1,并设置errno
代码示例
//man 7 ipstruct sockaddr_in ser,cli;bzero(&ser, sizeof(ser)); bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);//127.0.0.1 表示自己的IP地址,本机自己接收// ser.sin_addr.s_addr = inet_addr("127.0.0.1");ser.sin_addr.s_addr = INADDR_ANY;//三次握手成功判断int ret = connect(conn,(SA)&ser,sizeof(ser));if(-1 == ret){perror("connect fail");return 1;}
<3>send() 发送数据
<4>recv() 接收数据
<5>close() 关闭打开的套接字(触发四次挥手)
客户端代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>typedef struct sockaddr *SA;int main(int argc, char **argv)
{int conn = socket(AF_INET,SOCK_STREAM,0);if(-1 == conn){perror("socket fail");return 1;}//man 7 ipstruct sockaddr_in ser,cli;bzero(&ser, sizeof(ser)); bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);//127.0.0.1 表示自己的IP地址,本机自己接收// ser.sin_addr.s_addr = inet_addr("127.0.0.1");ser.sin_addr.s_addr = INADDR_ANY;//三次握手成功判断int ret = connect(conn,(SA)&ser,sizeof(ser));if(-1 == ret){perror("connect fail");return 1;}int i = 10;while (i--){char buf[1024] = "hello this is client";send(conn,buf,strlen(buf),0);bzero(buf, sizeof(buf));recv(conn,buf,sizeof(buf),0);printf("from ser: %s\n",buf);sleep(1);}close(conn); return 0;
}
数据粘包:因为tcp协议是流式套接字,数据与数据之间没有边界,发送方发送数据,接收方无法解析数据(接收方无法区分)
解决方案:
1.设置边界;
2.固定大小;
3.自定义协议;
TCP与UDP的区别
特性维度 | TCP (传输控制协议) | UDP (用户数据报协议) |
---|---|---|
连接方式 | 面向连接 | 无连接 |
传输数据前必须通过三次握手建立可靠连接 | 无需建立连接,可直接发送数据 | |
可靠性 | 安全可靠 | 不安全、不可靠 |
通过应答确认、超时重传、流量控制、拥塞控制等机制保证数据不丢失、不重复、按序到达。 | 不提供任何可靠性保证,可能丢失、重复、乱序。 | |
传输速度 | 相对较慢 | 非常快 |
由于复杂的控制机制和握手过程,延迟较高,传输速度慢。 | 没有连接开销和复杂控制,延迟极低,传输速度快。 | |
复杂度 | 机制复杂 | 实现简单 |
实现复杂,需要维护连接状态、序列号、确认号、窗口等。 | 协议本身非常简单,几乎只有端口和校验和。 | |
通信模式 | 全双工通信 | 支持全双工,但本质是报文交换 |
建立连接后,双方可同时可靠地发送和接收数据。使用发送缓冲区和接收缓冲区。 | 可以发送和接收,但每个数据包都是独立的。 | |
数据形式 | 字节流 | 数据报 |
无消息边界,应用程序看到的是一串无结构的字节流,需自行处理粘包/拆包问题。 | 有消息边界,接收到的数据与发送时完全一致,保留报文长度。 | |
控制机制 | 拥有完整的流量控制、拥塞控制、差错处理机制。 | 无拥塞控制。网络拥堵时发送速率不变,会导致更高丢包率。 |
传输单元 | 段 | 数据报 |
广播/组播 | 不支持 | 支持 |
仅支持一对一通信。 | 支持一对一、一对多(广播)、多对多(组播)通信。 | |
首部开销 | 大 | 小 |
标准首部20字节,加上可选选项可达60字节。 | 固定首部仅8字节。 | |
典型应用 | 要求数据完整、可靠的应用: • Web浏览 • 电子邮件 • 文件传输 • 数据库操作 | 要求低延迟、实时性的应用: • 视频会议、直播 • 语音通话 • 在线游戏 • DNS查询 • SNMP网络管理 |
套接字类型 | SOCK_STREAM (流式套接字) | SOCK_DGRAM (数据报套接字) |
数据报:
发送次数和接收次数需要对应;
数据与数据之间有边界