【学习记录】C/C++网络编程初步
Sunday, June 4, 2023
本文共2142字
5分钟阅读时长
⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95cc++%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%88%9D%E6%AD%A5/。商业转载请联系作者获得授权,非商业转载请注明出处!
One may say the eternal mystery of the world is its comprehensibility.
— Albert Einstein
学习资料
非常感谢大佬的教程~简单易懂~我推荐和我一样打算入门socket并快速上手的同学都看一看~
相关的理论知识在这些资料里面已经说的很清楚了,我这里就不再赘述了,仅仅简单地说一说TCP协议下的通信流程
通信流程
是时候祭出这张图了,图源https://subingwen.cn/linux/socket/tcp.jpg
简单来说,就是服务器要先建立套接字(表现为一个用于监听的文件描述符)并将socket文件描述符绑定连接端口,然后监听连接端口是否有新的客户端试图连接(listen设置端口监听,accept阻塞地等待客户端连接),然后假如有客户端通过这个端口连接,accept就取消阻塞,同时设置一个结构体(struct sockaddr_in),我们可以通过这个结构体知道客户端的相关信息;同时accept还会返回一个用于通信的文件描述符,我们可以通过这个文件描述符来与客户端使用recv/send通信。最后如果客户端有序关闭了(orderly shutdown)了,那么recv就会返回0,这时我们就知道客户端结束连接了(如果出现错误就会返回-1并设置errno)
然后客户端就比较简单,只需要建立socket文件描述符,在通过connect连接既定的IP:port
后直接向这个socket文件描述符进行读写(recv/send)就可以了
如何查看相关API
非常建议使用Linux的man命令查看!虽然是英文的,但是对于正在使用的系统是绝对权威的,而且无需联网,速度很快!
如果你想使用vim查看man手册(因为你的vim可能设置了习惯的快捷键),在此自荐我写的脚本,可以使用vim在man、tldr、cppman之间查询:使用vim在man、tldr、cppman库间搜索帮助手册并打开 - P3troL1er 的个人博客
完整代码
因为我写了详细的注释,所以我就不再赘述一遍了,如果对api有疑问建议使用man手册查看
下面是完整的C语言代码,本来是想写C++代码的,但是由于教程是C语言的,不知不觉几乎完全是C-style的代码,索性就直接写成C语言代码了
server.c:
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
// 几个宏用来防止手动写入过多错误判断代码
#define CKE(EXP) do {\
if ((EXP) == -1) {\
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
exit(127+errno);\
}\
} while(0)
#define CKEN(EXP) do {\
if ((EXP) != 0) {\
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
exit(127+errno);\
}\
} while(0)
// 用来保存通信线程所需的相关信息
typedef struct{
int cfd; // 通信的文件描述符
struct sockaddr_in details; // 客户端的相关信息
socklen_t details_len; // 上面那个结构体的长度,如果通信线程调用recvfrom就可以用到
char ip[16]; // 保存客户端的IP字符串
int port; // 保存客户端的端口
} netfd;
static void* communicate(void* arg){
// 通过强制转换
netfd* netinfo = (netfd*)arg;
// 每个已连接的通信线程都有1kb的缓冲区
char buf[1024] = {0};
// 固定的回复
const char* response = "server's spell...";
// TODOrecv & send
while (true) {
// 获取收到的字节数量
int len = recv(netinfo->cfd, buf, sizeof(buf), 0);
/**
关于recv的返回值,可以参考Linux的man手册:
These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error.
When a stream socket peer has performed an orderly shutdown, the return value will be 0 (the traditional "end-of-file" return).
Datagram sockets in various domains (e.g., the UNIX and Internet domains) permit zero-length datagrams. When such a datagram is received, the return value is 0.
The value 0 may also be returned if the requested number of bytes to receive from a stream socket was 0.
*/
if (len > 0)
{
printf("client from %s:%d sent: %s\n", netinfo->ip, netinfo->port, buf);
send(netinfo->cfd, response, sizeof(char) * (strlen(response) + 1), 0);
} else if (len == 0) {
// TODO接收到客户端终止连接请求
puts("the client has ended the connection...");
break;
} else {
// 接收到错误就打印出来,并且退出线程
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
close(netinfo->cfd);
pthread_exit((void*)((long)127+errno));
}
// 一定要设置buf为0,不然可能和新信息混杂在一起
// 因为buf被初始化为0,所以memset放在循环末尾
memset(buf, 0, sizeof(buf));
}
// 当客户端正常退出时就关闭通信的文件描述符
close(netinfo->cfd);
pthread_exit((void*)((long)0));
}
static void sig_int(int signo){
printf("thread %ld received: %s\n", pthread_self(), strsignal(signo));
puts("server exiting...");
exit(signo);
}
int main(int argc, char const *argv[])
{
// 声明局部变量
int lfd;
pthread_attr_t tid_attr;
pthread_t tid;
int err;
struct sockaddr_in server_addr; // 保存服务器的相关信息
const int connection_num = 128; // 设置最大连接数
// 设置信号处理函数
signal(SIGINT, sig_int);
// TODO设置监听的套接字
// 设置TCP协议
CKE(lfd = socket(AF_INET, SOCK_STREAM, 0));
// TODO绑定端口
server_addr.sin_family = AF_INET;
// from host byte order to network byte order
server_addr.sin_port = htons(10000);
// 代表本机的任意地址
// 或者设置其中一个IP,使用inet_pton将IP地址字符串转换为二进制格式
// inet_pton - convert IPv4 and IPv6 addresses from text to binary form
server_addr.sin_addr.s_addr = INADDR_ANY;
CKE(bind(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
// TODO设置端口监听
CKE(listen(lfd, connection_num));
// 初始化线程属性,设置为线程启动即分离
CKEN((err = pthread_attr_init(&tid_attr)));
CKEN((err = pthread_attr_setdetachstate(&tid_attr, PTHREAD_CREATE_DETACHED)));
// TODO开始接收连接请求
// 阻塞直到有客户端连接
// 在栈空间中保存该结构体的大小,因为后面的api需要传指针进去,可能会更改这个值
// 不停地检测是否有连接
// 如果没有连接就阻塞
while (true) {
// 这个结构体包括了传给线程的参数,因此不能是局部变量,可以在堆上分配
netfd* netinfo = (netfd*)malloc(sizeof(netfd));
netinfo->details_len = sizeof(netinfo->details);
/**
On success, these system calls return a file descriptor for the accepted socket (a nonnegative integer). On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
*/
CKE(netinfo->cfd = accept(lfd, (struct sockaddr*)&(netinfo->details), &(netinfo->details_len)));
// 此时应该已经收到了客户端的相关信息
netinfo->port = ntohs(netinfo->details.sin_port);
// 打印客户端相关信息
printf(
"client's IP: %s, port: %d\n",
inet_ntop(AF_INET, &(netinfo->details.sin_addr.s_addr), netinfo->ip, netinfo->details_len),
netinfo->port
);
// 创建信息
CKEN(err = pthread_create(&tid, &tid_attr, communicate, (void*)netinfo));
}
// TODO关闭两个端口
// 在主线程只用关闭监听的文件描述符
close(lfd);
return 0;
}
client.c:
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define CKE(EXP) do {\
if ((EXP) == -1) {\
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", #EXP, strerror(errno));\
exit(127+errno);\
}\
} while(0)
int main(int argc, char const *argv[])
{
// 声明局部变量
int fd;
char buf[1024] = {0};
// TODO 建立套接字
CKE(fd = socket(AF_INET, SOCK_STREAM, 0));
// TODO 建立连接
// 设置服务器地址、端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(10000);
inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr.s_addr));
CKE(connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr)));
//TODO send & recv
for(int i = 0; i < 100; i++)
{
int num = snprintf(buf, sizeof(buf)/sizeof(char)-1, "Massage NO.%d", i);
send(fd, buf, sizeof(char)*(num+1), 0);
memset(buf, 0, sizeof(buf));
if ((num = recv(fd, buf, sizeof(buf), 0)) > 0)
{
printf("server respond: %s\n", buf);
}else if (num == 0) {
puts("the server has ended its service...");
break;
}else {
fprintf(stderr, "[EXIT ERROR] %s raise error: %s\n", "recv()", strerror(errno));
exit(127+errno);
}
sleep(1);
}
// TODO 关闭连接
close(fd);
return 0;
}
效果图:
扫码阅读此文章
点击按钮复制分享信息
点击订阅