linux——TCP编程

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_INETIPv4 网络通信(99% 用这个)
  • AF_INET6 → IPv6 通信

其他暂时不用管:

  • AF_UNIX:本机进程间通信
  • AF_PACKET:底层网卡抓包
  1. type:通信方式(最重要!)
  • SOCK_STREAM流式套接字 → 对应 TCP 协议→ 可靠、连接、有序

  • SOCK_DGRAM数据报套接字 → 对应 UDP 协议→ 不可靠、无连接、快

  • SOCK_RAW→ 原始套接字(自己写协议)

  1. 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 和端口号。 = 给手机绑定手机号

参数

  1. sockfd

socket () 返回的文件描述符。

  1. 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;
};

三个必须赋值的东西:

  1. sin_family = AF_INET
  2. sin_port = 端口号(要转网络字节序)
  3. 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 服务器雏形

  1. 向操作系统申请一个通信套接字(socket)
  2. 给这个套接字绑定固定的 IP 和端口
  3. 绑定成功后,这个程序就可以等待客户端连接

①创建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转网络格式

两个超级关键函数

  1. htons() 主机字节序 → 网络字节序端口必须用这个函数!

  2. 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),意思是:

内核会自动维护 两个队列

  1. **半连接队列(SYN 队列)**客户端发了第一次握手,还没完成连接。
  2. 全连接队列(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 的区别(必须背)

  1. sockfd(监听 fd)

    • 只负责等连接
    • 不负责通信
  2. 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);
  1. **bzero**把缓冲区清空,避免上次残留的数据干扰。

  2. 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);

参数

  1. sockfd
  • 就是你用 socket() 创建出来的文件描述符
  • 作用:这是我用来打电话的手机
  1. serv_addr(最重要!)

这是目标服务器的地址结构体,里面必须放 3 样东西:

  1. sin_family = AF_INET
  2. sin_port = 服务器的端口(htons 转换)
  3. sin_addr.s_addr = 服务器的IP

一句话:

你要连接谁,就把谁的地址填在这里!

  1. addrlen
  • 地址结构体的长度

  • 固定写法:

    复制代码
    sizeof(struct sockaddr_in)

返回值

  • 成功返回 0 → 电话打通了
  • 失败返回 -1 → 没打通(服务器没开、IP 错、端口错、防火墙拦了)

connect () 到底干了什么大事?(底层必懂)

它在底层自动完成 TCP 三次握手!

你不用写任何代码,它自己做:

  1. 客户端 → 发送 SYN 包 → 服务器
  2. 服务器 → 回复 SYN + ACK → 客户端
  3. 客户端 → 回复 ACK → 服务器

握手完成 → connect 返回 0 → 连接成功!

最关键区别

服务器:bind + listen + accept

客户端:connect

  • 服务器不主动连接 ,只负责等连接
  • 客户端主动连接 ,用 connect

connect 什么时候会失败?

  1. 服务器没启动
  2. IP 写错
  3. 端口写错
  4. 防火墙禁止连接 只要有一个错,直接返回 -1,报错:Connection refused(连接被拒绝)

总结

  1. connect 是客户端专属函数
  2. connect 作用:主动连接服务器
  3. connect 内部自动完成三次握手
  4. 成功返回 0,失败返回 -1
  5. 第二个参数必须填「服务器的 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 客户端

  1. 连接到 192.168.88.129:5001 服务器
  2. 从键盘输入内容
  3. 发送给服务器
  4. 输入 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:IPv4
  • SOCK_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;
    }
}

功能解释:

  1. fgets:从键盘获取你输入的内容
  2. write:把内容发送给服务器
  3. strncasecmp:比较是否是退出指令(不区分大小写)

运行结果

相关推荐
云栖梦泽2 小时前
Linux内核与驱动:9.驱动中的中断机制
linux
格林威2 小时前
Windows 实时性补丁(RTX / WSL2)
linux·运维·人工智能·windows·数码相机·计算机视觉·工业相机
xuxie992 小时前
N22 key驱动
linux·运维·服务器
大地的一角2 小时前
(计算机网络)网络层原理与网络大致结构
服务器·网络·tcp/ip
c++逐梦人2 小时前
Linux多线程
linux·服务器
开心码农1号2 小时前
RabbitMQ 生产运维命令大全
linux·开发语言·ruby
IMPYLH2 小时前
Linux 的 nl 命令
linux·运维·服务器·bash
咖喱o2 小时前
路由策略
linux·服务器·网络
南境十里·墨染春水2 小时前
linux学习进展 主函数的参数
linux·运维·学习