📌 个人主页: 孙同学_
🔧 文章专栏: Liunx
💡 关注我,分享经验,助你少走弯路!
文章目录
-
- Socket编程TCP
-
- [TCP Socket 编写流程](#TCP Socket 编写流程)
- [socket 创建套接字](#socket 创建套接字)
- [bind 端口号](#bind 端口号)
- [listen 监听](#listen 监听)
- [accept 获取链接](#accept 获取链接)
- [read 读数据](#read 读数据)
- [write 发数据](#write 发数据)
- [connect 客户端向服务器发起建立连接的请求](#connect 客户端向服务器发起建立连接的请求)
- 简单演示
Socket编程TCP
TCP是一种有连接、面向字节流、可靠的传输层协议。在Linux环境下进行TCP Socket编程,其实就是编写一套双方如何通过网线打电话 的代码逻辑。
我们可以把**Socket(套接字)**想象成一个电话。Server(服务端)就像是10086的接线员,Client(客户端)就是打电话的用户。
TCP变成分为:
- 服务端
- 客户端
TCP Socket 编写流程
核心步骤:
| 角色 | 动作 | 函数名称 | 含义 |
|---|---|---|---|
| Server | 创建电话机 | socket() | 申请一个网络通信的文件描述符。 |
| Server | 固定电话号 | bind() | 把ip地址和端口号绑定到socket上 。 |
| Server | 开启铃声 | listen() | 进入监听状态,准备接听电话 。 |
| Server | 接听电话 | accept() | 阻塞等待,直到有客户端打进来,返回一个新的连接。 |
| Clinet | 拨号 | connect() | 主动连接指定ip和端口 |
| Both | 通话 | read() / write() | 双方收发数据(在Linux中,Socket被视为文件) |
| Both | 挂断 | close() | 释放资源,断开连接。 |
服务器往往是进禁止拷贝的
我们可以直接定义一个Nocopy的类
cpp
#pragma once
#include <iostream>
class nocopy
{
public:
nocopy() {}
nocopy(const nocopy &) = delete; //把nocopy的拷贝构造删掉
const nocopy &operator=(const nocopy &) = delete;//把nocopy的拷贝赋值语句也删掉
~nocopy() {}
};
};

要形成TcpServer的拷贝,就得先把基类拷贝一下,而基类是禁止去拷贝和赋值的。
socket 创建套接字


第三个参数我们直接设置为0.
返回值:成功返回一个文件描述符,失败直接返回-1
bind 端口号

listen 监听

第一个参数:我们要监听的文件的文件描述符sockfd,
第二个参数:允许底层排队的最大链接数
返回值:成功0被返回,失败-1被设置
netstat -tnlp
t表示显示的协议都是tcp的
n表示显示成数字
l表示listen状态的 a表示所有的
p表示进程相关的信息
telnet 表示可以远程登录我们指定的tcp服务例如:telnet baidu.com 80
结论:只要服务器处于listen状态,那么它就已经可以被连接了
为什么今天查到的是两条连接?

accept 获取链接

从内核中直接获取,而建立连接的过程和accept无关.
第二个参数和第三个参数是输出型参数,等同于recvfrom的后两个参数。
返回值:成功,这个系统调用会返回一个合法的整数,它是一个文件描述符。失败返回-1,错误码被设置。
这个文件描述符和sockfd有何区别??
好再来鱼庄就是我们自己写的服务器,在马路边上拉人,这个马路就相当于操作系统内部,张三相当于我们创建的_sockfd,这种_sockfd只负责在操作系统内部获取链接,我们把这种获取链接的套接字叫做listensockfd.当我们accept的时候,accept的动作就相当于拉客的动作。拉到客人提供的服务由accept的返回值来提供,这个服务通常是读写服务。
UDP是面向数据报,读取时要用特定的接口recvfrom,我们读取TCP的时候,用的接口是read。
read 读数据

返回值:读成功了返回实际读到的字节数,读失败会有等于0的情况和小于0的情况
返回值:通常有三种情况
a. n>0:读取成功
b. n<0:读取失败
c. n==0:我在读取的时候,对端把连接关了,相当于服务器读到了文件的结尾 (管道pipe:一i个新打开的管道,如果读端一直在读,写端不写了,写端不写,读端就会一直阻塞等它写。如果它不光不写了,并且还把写端关闭了,读就会读到0,表明读到文件结尾)
write 发数据

connect 客户端向服务器发起建立连接的请求

返回值:成功0被返回。否则-1被返回
问题1: 进程如果退出了,曾经打开的文件会怎么办?
默认会被自动释放,文件描述符会被自动关闭。
问题2: 进程如果打开了一个文件,得到了一个fd,如果再创建子进程,这个子进程能拿到父进程的fd进行访问吗?
能

问题1: 如果进程打开了一个文件,得到了一个fd,这个fd线程能看见吗?
答案是能看到,因为创建线程的时候,只是创建了TCB,并没有创建对应的文件描述符表,文件描述符表属于被所有线程共享的。
问题2: 线程敢不敢关闭自己不需要的fd?为什么?
不敢,因为线程已经共享了,所以我们就不能像多进程那样关闭不需要的文件描述符。
简单演示
服务端(server.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
// 1. 创建 socket (IPv4, TCP协议)
int serv_sock = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定 IP 和端口
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有IP
serv_addr.sin_port = htons(8888); // 监听8888端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 3. 监听
listen(serv_sock, 5);
printf("服务器启动,正在等待连接...\n");
// 4. 接受连接
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
printf("客户端已连接!\n");
// 5. 读取数据并打印
char buffer[1024];
read(clnt_sock, buffer, sizeof(buffer)-1);
printf("收到消息: %s\n", buffer);
// 6. 关闭
close(clnt_sock);
close(serv_sock);
return 0;
}
客户端(client.c)
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
// 1. 创建 socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务器 (连接本地 127.0.0.1)
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// 3. 发送数据
char *msg = "Hello, Linux Network!";
write(sock, msg, strlen(msg));
printf("消息已发送!\n");
// 4. 关闭
close(sock);
return 0;
}
👍 如果对你有帮助,欢迎:
- 点赞 ⭐️
- 收藏 📌
- 关注 🔔
