Linux网络编程1:C语言服务器端与客户端案例详解
图示流程
1 客户端简单代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#define SERV_PORT 9000 //要连接到的服务器端口,服务器必须在这个端口上listen着
int main(int argc, char *const *argv)
{
//这些演示代码的写法都是固定套路,一般都这么写
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建客户端socket,地址家族,套接字类型,套接字协议
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
//设置要连接到的服务器的信息
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //连接到的服务器端口,服务器监听这个地址
//这里为了方便演示,要连接的服务器地址固定写
if(inet_pton(AF_INET,"192.168.1.126",&serv_addr.sin_addr) <= 0) //IP地址转换函数,把第二个参数对应的ip地址转换第三个参数里边去,固定写法
{
printf("调用inet_pton()失败,退出!\n");
exit(1);
}
//连接到服务器
if(connect(sockfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0)
{
printf("调用connect()失败,退出!\n");
exit(1);
}
int n;
char recvline[1000 + 1];
while(( n = read(sockfd,recvline,1000)) > 0) //仅供演示,非商用,所以不检查收到的宽度,实际商业代码,不可以这么写
{
recvline[n] = 0; //实际商业代码要判断是否收取完毕等等,所以这个代码只有学习价值,并无商业价值
printf("收到的内容为:%s\n",recvline);
}
close(sockfd); //关闭套接字
printf("程序执行完毕,退出!\n");
return 0;
}
1.1 拆分步骤
1.1.1 创建socket
作用为确定连接的类型
int socket(int domain, int type, int protocol);
- 参数:
1.domain:协议族,协议族决定了socket的地址类型,在通信中必须采用相应的地址。
2.type: 指定socket的数据传输方式(因为domain的协议族也可能有多个传输方式):
- SOCK_STREAM:基于TCP的流格式套接字,不存在数据边界(发送方发多少次,接收方可以只收一次)
- SOCK_DGRAM:基于UDP的数据报格式套接字,存在数据边界(发送方发多少次,接收方收多少次)
3.protocol:常见的协议有IPPROTO_TCP、IPPROTO_UDP、 IPPROTO_SCTP、IPPROTO_TIPC他们分别对应这TCP传输协议,UDP传输协议,STCP传输协议,TIPC传输协议。当protocol为0时,会自动选择type类型对应的默认协议。
- 返回值
成功时返回一个唯一整数【文件描述符】,失败时返回-1
-
注意事项
第三个参数大多数时候传0,即保持默认,除非遇到下面的情况:
同一协议族中存在多个 数据传输方式 相同的协议(即指定了domain和type,仍不能确定某一协议)
-
例子
// IPv4家族中面向连接的套接字, IPPROTO_TCP可省略填0
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
// IPv4家族中面向消息的套接字,同样可省略
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)
1.1.2 创建结构体
用于存放要连接的IP地址和端口号
此部分需要注意两种结构体sockaddr和sockaddr_in。直接向sockaddr写入IP和端口信息较为麻烦,而设置sockaddr_in较为方便;但由于后续需要sockaddr,而两种类型是相似的,直接强制类型转换即可。
另外,sockaddr_in是表示IPv4的结构体,但为什么还要地址族sa_family_t。这是为了和sockaddr保持一致,而sockaddr并非只为IPv4设计。
a、结构体细节
//早期的sockaddr ,并非只为IPv4
struct sockaddr
{
sa_family_t sa_family; /* adress family: AF_XXX */
char sa_data[14];/* 14 bytes of protocol */
};
//IPv4的sockaddr
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号,以网络字节序保存
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充,目的是与sockaddr结构体保持一致
};
// 其中in_addr
struct in_addr
{
in_addr_t s_addr; /*32-bit IPV4 address*/
};
b、使用方式
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //连接到的服务器端口,服务器监听这个地址
inet_pton(AF_INET,"192.168.1.126",&serv_addr.sin_addr) // 设置IP地址
c、端口要转化为网络字节顺序
Linux网络编程2: IP地址的字符串 与 端口号的主机字节序 的 网络字节序转换
include <arap/inet.h>
uint16_t htons(uint16_t hostshort);
htons是将 short变量 从主机字节顺序(大小端)转变成网络字节顺序(大端),大端 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
为什么用16位?因为计算机端口数量就是65536个/2^16个
d、字符串ip地址转为网络字节序整数
Linux网络编程2: IP地址的字符串 与 端口号的主机字节序 的 网络字节序转换
static int inet_pton(int af, const char *src,void *dst)
af: address family(协议族)
src:是个指针,指向保存IP地址字符串形式的字符串。
dst:指向存放网络地址的in_addr结构体的首地址
sockaddr_in中保存IP地址信息的成员为32位整数,所以要将字符串形式的ip地址转化为整数数据。
转换方式1、IP字符串 to 网络字节序
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_network(const char *cp);
1.1.3 连接
connect并不意味着服务器端调用accept,可能会进入等待队列,这个函数的作用仅是通知 Linux 内核,让内核完成 TCP 三次握手连接。
connect会阻塞,直到连接成功/
客户端IP地址和端口在调用connect时分配,无需bind
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)
1、sockfd,套接字返回的整数
2、sockaddr,connect中要求的参数是sockaddr结构体,我们构建的是sockaddr_id,但两种结构体都是16字节,结构相似,可以进行强制类型转换。
3、sockaddr的长度,sizeof()即可
1.1.4 连接成功读取数据
int read(int handle,void *buf,int len);
1、int handle 为要读取的文件
2、void *buf 为要将读取的内容保存的缓冲区
3、int len 读取文件的长度
返回值是实际读取的字节数
2 服务器简单代码
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#define SERV_PORT 9000 //本服务器要监听的端口号,一般1024以下的端口很多都是属于周知端口,所以我们一般采用1024之后的数字做端口号
int main(int argc, char *const *argv)
{
//这些演示代码的写法都是固定套路,一般都这么写
//服务器的socket套接字【文件描述符】
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr; //服务器的地址结构体
memset(&serv_addr,0,sizeof(serv_addr));
//设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));//绑定服务器地址结构体
listen(listenfd, 32); //参数2表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成,c/s之间进入正常通讯后,请求数-1
int connfd;
const char *pcontent = "I sent sth to client!"; //指向常量字符串区的指针
for(;;)
{
//卡在这里,等客户单连接,客户端连入后,该函数走下去【注意这里返回的是一个新的socket——connfd,后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接】
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
//发送数据包给客户端
write(connfd,pcontent,strlen(pcontent)); //注意第一个参数是accept返回的connfd套接字
//只给客户端发送一个信息,然后直接关闭套接字连接;
close(connfd);
} //end for
close(listenfd); //实际本简单范例走不到这里,这句暂时看起来没啥用
return 0;
}
2.1 拆分步骤
2.1.1 监听socket的创建
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
同样需要协议族、协议类型
2.1.2 地址结构体
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 一个服务器可能有多块网卡,每个网卡也可能配置多个IP地址,所以用INADDR_ANY,表示服务器的所有可用IP地址,这样监听到9000端口,无论是哪个IP地址都能收到
2.1.3 绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:bind API能够将套接字文件描述符、端口号和ip绑定到一起。因为socket只指明了协议。
- 返回值:成功时返回0,失败返回-1
- 参数:
- sockfd: 表示socket函数创建的通信文件描述符
- addr: struct sockaddr_in或sockaddr的首地址,用于设定要绑定的ip和端口
- addrlen: 表示所指定addr结构体的大小
2.1.4 监听
listen()函数不会阻塞,它仅将该套接字和套接字对应的连接队列长度告诉 Linux 内核后结束。当客户端connect,内核自动完成三次握手
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- 功能:
将套接字文件描述符从主动(主动向对方发送数据)转为被动文件描述符,然后用于被动监听客户端的连接 - 参数:
- sockfd 表示socket创建的套接字文件描述符
- backlog 指定队列的容量用于记录正在连接但是还没有连接完成的客户端,一般设置队列的容量为2,3即可。队列的最大容量需要小于30
- 返回值:成功返回0,失败返回-1, errno被设置
2.1.5 TCP连接(受理请求)
注意两个套接字,socket生成的套接字是用于bind绑定和listen监听和accept受理请求,而accept生成的套接字用于和客户端交换数据。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 函数功能:
从established 状态的连接队列头部取出一个已经完成的连接,产生用于数据IO的套接字(返回值),套接字文件描述符默认是阻塞的,即如果没有客户端请求连接的时候,此时accept会阻塞,直到有客户端连接;如果不想套接字文件描述符阻塞,则可以创建套接字 socket函数 时指定type为SOCK_NOBLOCK。 - 函数返回值:成功返回套接字描述符,失败返回-1
- 参数
- sockfd 表示socket创建的套接字文件描述符
- addr: 用于记录发起连接请求的那个客户端的IP端口
- addrlen表示第二个参数addr的大小
2.1.6 数据传输
int write(int handle,void *buf,int len);
和read类似。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战