TCP编程API

socket = 买手机
bind = 给手机插卡绑定手机号
有了手机+号码,别人才能联系你
网络编程里:
服务器必须socket+bind
客户端只需要socket,不需要bind
服务器端
1.socket函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
作用:
创建一个 "通信端点",相当于买一部手机。 成功返回一个 文件描述符 fd(像手机编号)。
参数:
1、 domain:你用哪种 "通信家族"
最常用就 2 个:
- AF_INET → IPv4 网络通信(99% 用这个)
- AF_INET6 → IPv6 通信
其他暂时不用管:
- AF_UNIX:本机进程间通信
- AF_PACKET:底层网卡抓包
- type:通信方式(最重要!)
-
SOCK_STREAM → 流式套接字 → 对应 TCP 协议→ 可靠、连接、有序
-
SOCK_DGRAM → 数据报套接字 → 对应 UDP 协议→ 不可靠、无连接、快
-
SOCK_RAW→ 原始套接字(自己写协议)
- protocol:几乎永远填 0
因为:
- SOCK_STREAM 自动对应 TCP
- SOCK_DGRAM 自动对应 UDP所以 写 0 就行!
返回值
- 成功:返回 文件描述符(小整数,类似 3、4、5)
- 失败:返回 -1
如果是IPV6编程,要使用struct sockddr_in6结构体(man 7 IPV6),通常使用struct sockaddr_storage来编程。
2、bind()------绑定IP+端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:
给 socket 绑定 IP 和端口号。 = 给手机绑定手机号。
参数
- sockfd
socket () 返回的文件描述符。
- addr:最重要!结构体
系统提供一个通用结构体:
struct sockaddr {
sa_family_t sa_family; // 地址家族 AF_INET
char sa_data[14]; // 存放 IP + 端口
};
这个结构体难用、难看、不直观,所以我们用:
实际开发用:IPv4 专用结构体
struct sockaddr_in {
short sin_family; // AF_INET
uint16_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 填充,必须填0
};
struct in_addr {
uint32_t s_addr;
};
三个必须赋值的东西:
sin_family = AF_INETsin_port = 端口号(要转网络字节序)sin_addr.s_addr = IP地址
超级重点:为什么要转字节序?
因为:
- 电脑是 小端
- 网络是 大端
所以必须用函数转换:
htons(port); // 主机端口 → 网络端口
IP 地址怎么填?
服务器通常填:
INADDR_ANY
意思:本机所有网卡 IP 都可以连接我= 0.0.0.0
addrlen 地址长度,直接写:
sizeof(struct sockaddr_in)
总结:
-
socket = 创建通信端点
-
bind = 给端点绑定 IP + 端口
-
服务器必须 bind,客户端不用
-
sockaddr_in 是给人用的,sockaddr 是给系统用的
-
端口必须 htons (),IP 可以 INADDR_ANY
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include<stdlib.h>
#include <strings.h>
#include <arpa/inet.h>#define SERV_IP 5001
#define SERV_IP_ADDR "192.168.88.129"
int main()
{
int fd = -1;struct sockaddr_in sin; //1.socket fd = socket(AF_INET,SOCK_STREAM,0); if(fd<0) { perror("socket"); exit(1); } bzero(&sin,sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(SERV_IP); sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR); /*if(inet_pton(AF_INET,SERV_IP_ADDR,&sin.sin_addr.s_addr) != 1) { perror("inet_pton"); exit(1); } */ //2.bind if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) <0) { perror("bind"); exit(1); } return 0;}
这是一个最简单的 TCP 服务器雏形
- 向操作系统申请一个通信套接字(socket)
- 给这个套接字绑定固定的 IP 和端口
- 绑定成功后,这个程序就可以等待客户端连接了
①创建socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
解释:
AF_INET:使用 IPv4 协议SOCK_STREAM:使用 TCP 流式套接字0:自动匹配协议
返回值:
- 成功:返回一个文件描述符 fd(整数,类似 3、4、5)
- 失败:返回 -1
②清空结构体
bzero(&sin, sizeof(sin));
把结构体里的垃圾数据全部清 0,避免绑定失败。
③给结构体赋值(填IP、端口、协议)
sin.sin_family = AF_INET; // 固定填 IPv4
sin.sin_port = htons(SERV_IP); // 端口必须转字节序!
sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR); // IP转网络格式
两个超级关键函数
-
htons() 主机字节序 → 网络字节序端口必须用这个函数!
-
inet_addr() 字符串 IP → 网络格式 IP
"192.168.88.129"→ 变成二进制
这种字符IP转为网络IP的优点是简单,但不支持IPv6,出错时返回 INADDR_NONE 也就是 0xffffffff,和广播地址冲突,现代不推荐
inet_pton(AF_INET, SERV_IP_ADDR, &sin.sin_addr);
还可以用inet_pton,这种方式支持支持 IPv4 和 IPv6,有明确返回值,出错好判断,这是现代 Linux 官方推荐写法,没有歧义,不会和广播地址冲突。成功 → 返回 1,地址错误 → 返回 0,函数失败 → 返回 -1
④绑定bind
bind(fd, (struct sockaddr *)&sin, sizeof(sin));
解释:
fd:刚才创建的套接字&sin:IP + 端口信息- 必须强转成
struct sockaddr *(系统规定)
3、listen()函数
int listen(int sockfd, int backlog);
作用
把你的 socket 从 **"主动去连别人"** 改成**"被动等别人来连我"**。
参数 sockfd
就是你 socket () 创建 + bind () 绑定 好的那个文件描述符。这个 fd 从此变成监听专用。
参数 backlog
backlog = 等待握手的队列长度
写 listen(fd, 5),意思是:
内核会自动维护 两个队列
- **半连接队列(SYN 队列)**客户端发了第一次握手,还没完成连接。
- 全连接队列(ACCEPT 队列) 已经完成三次握手,等着被 accept () 取走的连接。
系统实际能同时处理的连接数
plaintext
2 * backlog + 1
你写 5 → 系统允许 11 个客户端 同时在排队握手。
为什么是 2*backlog+1?
因为:
- 一半在握手中
- 一半在已完成握手,等待 accept
- +1 是预留位
listen 总结(最简单记忆)
- listen 不阻塞!
- listen 只是设置队列长度!
- listen 之后,服务器才算真正可以被连接
4、accept()------阻塞等待客户端连接
int accept(
int sockfd,
struct sockaddr *addr,
socklen_t *addrlen
);
作用
站在门口死等,客户端不来,我就不动。
重点:accept 是阻塞函数
-
没有客户端连接 → 卡在这里不动
-
客户端来了 → 立刻往下执行
参数解释
sockfd
监听 fd(经过 socket、bind、listen 的那个)
addr
输出型参数 → 用来保存客户端的 IP 和端口你传一个结构体地址进去,函数会自动把客户端信息填进去。
addrlen
**必须是指针!**必须先赋值长度:
socklen_t len = sizeof(client_addr);
accept 的返回值 ------ 最重要!
accept 成功会返回一个 全新的文件描述符!
两个 fd 的区别(必须背)
-
sockfd(监听 fd)
- 只负责等连接
- 不负责通信
-
cfd(新连接 fd)
- 专门和客户端通信
- read /write 都用它!
接着上面那段代码
//3.listen
if(listen(fd,BACKLOG) < 0)
{
perror("listen");
exit(1);
}
//4.accept
int newfd = -1;
newfd = accept(fd,NULL,NULL);
if(newfd < 0)
{
perror("accept");
exit(1);
}
listen(fd, BACKLOG) 到底在干嘛?
-
fd:创建好、绑定好 IP / 端口 的套接字
-
BACKLOG:队列长度(一般写 5、10、128)
作用:
告诉操作系统: 这个 fd 现在变成【监听套接字】,开始等待客户端连接!
关键点:
✅ listen 不阻塞✅ 执行完立刻往下走✅ 内核开始维护两个队列:半连接、全连接
accept 作用
阻塞等待客户端连接! 没有客户端 → 卡在这里不动 客户端来了 → 返回 newfd
最关键:newfd 是什么?
newfd 是 全新的套接字
专门用来 和客户端通信
以后 read / write 全都用 newfd!
老的 fd 继续等待下一个客户!
5、接收客户端数据
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
char buf[BUFSIZE];
int ret = -1;
//read
while(1)
{
do
{
bzero(buf,BUFSIZE);
ret = read(newfd,buf,BUFSIZE-1);
}while(ret < 1);
if(ret < 0)
{
exit(1);
}
if(!ret)
{
break;
}
printf("receive data:%s\n",buf);
if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
{
printf("Client is exiting!\n");
break;
}
}
这段代码的作用:
无限循环读取客户端发来的数据, 如果读到 "QUIT" 就退出,客户端断开也退出,出错直接退出。
①定义
#define QUIT_STR "QUIT" // 退出字符串:客户端发这个就断开
#define BUFSIZE 1024 // 缓冲区大小
char buf[BUFSIZE]; // 用来存放收到的数据
②定义变量
int ret = -1; // 用来保存 read() 的返回值
③大循环:一直读,永不停止
while(1) // 死循环:一直接收
{
④内层 do-while:保证读到有效数据
do
{
bzero(buf, BUFSIZE); // 清空缓冲区(非常重要)
ret = read(newfd, buf, BUFSIZE-1); // 读数据
} while(ret < 1);
-
**
bzero**把缓冲区清空,避免上次残留的数据干扰。 -
read(newfd, buf, BUFSIZE-1)
newfd:和客户端通信的套接字buf:存数据BUFSIZE-1:留一个位置给字符串结束符\0
3.while(ret < 1)
- 没读到数据(ret=0)
- 或者出错(ret<0)→ 继续读,直到读到有效数据
⑤出错判断
if(ret < 0)
{
exit(1); // read 失败,直接退出
}
⑥客户端断开链接
if(!ret) // ret == 0
{
break; // 客户端关闭了连接,退出循环
}
⑦打印收到的数据
printf("receive data:%s\n", buf);
⑧判断是否退出命令
if(!strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)))
{
printf("Client is exiting!\n");
break;
}
如果客户端发来的数据开头是 "QUIT"(不区分大小写), 那就退出循环,关闭连接。
函数解释:
strncasecmp:不区分大小写比较strlen(QUIT_STR):只比较前 4 个字符(Q U I T)- 返回 0 → 相等 →
!0→ 条件成立
客户端
客户端连接函数connect()
connect () = 客户端主动给服务器「打电话」!
- 服务器在listen() 等着接电话
- 客户端connect() 主动拨号
- 电话通了(三次握手成功),connect 就成功!
函数原型
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
参数
- sockfd
- 就是你用
socket()创建出来的文件描述符 - 作用:这是我用来打电话的手机
- serv_addr(最重要!)
这是目标服务器的地址结构体,里面必须放 3 样东西:
sin_family = AF_INETsin_port = 服务器的端口(htons 转换)sin_addr.s_addr = 服务器的IP
一句话:
你要连接谁,就把谁的地址填在这里!
- addrlen
-
地址结构体的长度
-
固定写法:
sizeof(struct sockaddr_in)
返回值
- 成功返回 0 → 电话打通了
- 失败返回 -1 → 没打通(服务器没开、IP 错、端口错、防火墙拦了)
connect () 到底干了什么大事?(底层必懂)
它在底层自动完成 TCP 三次握手!
你不用写任何代码,它自己做:
- 客户端 → 发送 SYN 包 → 服务器
- 服务器 → 回复 SYN + ACK → 客户端
- 客户端 → 回复 ACK → 服务器
握手完成 → connect 返回 0 → 连接成功!
最关键区别
服务器:bind + listen + accept
客户端:connect
- 服务器不主动连接 ,只负责等连接
- 客户端主动连接 ,用 connect
connect 什么时候会失败?
- 服务器没启动
- IP 写错
- 端口写错
- 防火墙禁止连接 只要有一个错,直接返回 -1,报错:
Connection refused(连接被拒绝)
总结
- connect 是客户端专属函数
- connect 作用:主动连接服务器
- connect 内部自动完成三次握手
- 成功返回 0,失败返回 -1
- 第二个参数必须填「服务器的 IP 和端口」
客户端用 connect 拨号,服务器用 accept 接电话,连接成功后,双方就可以 read/write 聊天了!
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.88.129"
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
int main()
{
int fd = -1;
struct sockaddr_in sin;
fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
perror("socket");
exit(1);
}
bzero(&sin,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
if(connect(fd,(struct sockaddr*)&sin,sizeof(sin)) < 0)
{
perror("connect");
exit(1);
}
char buf[BUFSIZE];
while(1)
{
bzero(buf,BUFSIZE);
if(fgets(buf,BUFSIZE-1,stdin) == NULL)
{
continue;
}
write(fd,buf,strlen(buf));
if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
{
break;
}
}
close(fd);
return 0;
}
整段代码的作用
这是一个TCP 客户端:
- 连接到 192.168.88.129:5001 服务器
- 从键盘输入内容
- 发送给服务器
- 输入 QUIT 就退出
①宏定义(配置信息)
#define SERV_PORT 5001 // 服务器端口
#define SERV_IP_ADDR "192.168.88.129" // 服务器IP
#define BUFSIZE 1024 // 缓冲区大小
#define QUIT_STR "QUIT" // 退出字符串
②创建 socket(买手机)
int fd = -1;
struct sockaddr_in sin;
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
exit(1);
}
AF_INET:IPv4SOCK_STREAM:TCP 协议- 返回文件描述符
fd,用于后续通信
③配置服务器地址(填对方号码)
bzero(&sin, sizeof(sin)); // 清空结构体
sin.sin_family = AF_INET; // IPv4
sin.sin_port = htons(SERV_PORT); // 服务器端口
sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR); // 服务器IP
客户端必须填服务器的 IP 和端口!
④connect () 连接服务器(拨号!)
if(connect(fd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(1);
}
- 主动连接服务器
- 内部完成三次握手
- 成功返回 0,失败返回 - 1
⑤循环从键盘读取并发送数据(核心逻辑)
char buf[BUFSIZE];
while(1)
{
bzero(buf, BUFSIZE); // 清空缓冲区
// 从键盘读入
if(fgets(buf, BUFSIZE-1, stdin) == NULL)
{
continue;
}
// 发送给服务器
write(fd, buf, strlen(buf));
// 如果输入 QUIT,退出
if(!strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)))
{
break;
}
}
功能解释:
fgets:从键盘获取你输入的内容write:把内容发送给服务器strncasecmp:比较是否是退出指令(不区分大小写)
运行结果
