前言
本文的内容是多人聊天室的前期工作,记录的是一个客户端和一个服务端聊天,尚未涉及到多线程。若想看多线程的内容,请看下篇。
一、聊天模式框架
先把一个服务端和客户端聊天框架先构建好
二、代码
服务端(server.c)代码如下(示例):
/************************************************************************
*
* 文件名:server.c
*
* 功能:服务端(一般用于接收数据)
*
* 创建人:LZH
*
* 时间:2021年11月13日22:53:00
*
* 版本号:1.0
*
* 修改记录:无
*
************************************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//1、准备一个未连接的套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2、绑定一个IP地址到套接字上
struct sockaddr_in seraddr;
socklen_t len = sizeof(seraddr);
bzero(&seraddr, len); //赋值前先清空seraddr内存上数据
seraddr.sin_family = AF_INET; //设置服务器协议
seraddr.sin_port = htons(atoi(argv[1])); //设置服务器的端口号(把字符串转成短整型)
seraddr.sin_addr.s_addr = htonl(INADDR_ANY); //设置服务器的IP地址
bind(sockfd, (struct sockaddr *)(&seraddr), len);
//3、将未连接套接字转换成监听套接字
listen(sockfd, 5);
//4、不断等待监听套接字上的数据
struct sockaddr_in cliaddr;
bzero(&cliaddr, len);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); //谁连接到服务器上,谁的信息就会保存到这个变量中。
if(connfd > 0) {
printf("new connection:%d\n", connfd);
} else {
printf("accept function error!\n");
}
//5. 畅聊 不断接受客户端发送过来的数据
char buf[100];
while(1) {
//5.1 清空缓冲区
bzero(buf, sizeof(buf));
//5.2 把已连接套接字上的数据读取到缓冲区
recv(connfd,buf,sizeof(buf),0);
//5.3 将缓冲区的数据打印出来
printf("from client: %s",buf);
//5.4 如果客户端给我发了quit,那么我就退出
if( strncmp(buf,"quit",4) == 0 )
{
break; //如果收到了quit,则跳出循环,程序结束
}
}
//6. 挂断
close(connfd);
close(sockfd);
return 0;
}
客户端(client.c)代码如下(示例):
/************************************************************************
*
* 文件名:client.c
*
* 功能:服务端(一般用于接收数据)
*
* 创建人:LZH
*
* 时间:2021年11月14日00:29:46
*
* 版本号:1.0
*
* 修改记录:无
*
************************************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//1、准备一个未连接的套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2. 准备服务器的地址
struct sockaddr_in seraddr;
socklen_t len = sizeof(seraddr);
bzero(&seraddr, len); //赋值前先清空seraddr内存上数据
seraddr.sin_family = AF_INET; //设置服务器协议
seraddr.sin_port = htons(atoi(argv[2])); //设置服务器的端口号(把字符串转成短整型)
inet_pton(AF_INET, argv[1], &seraddr.sin_addr); //设置服务器IP地址
//3. 直接发起连接
int ret = connect(sockfd, (struct sockaddr *)(&seraddr), len);
if(ret == 0) {
printf("connect success!\n");
} else {
printf("connect fail!\n");
}
//4. 不断发送数据给服务器。
char buf[100];
while(1)
{
//4.1 先清空缓冲区
bzero(buf, sizeof(buf));
//4.2 从键盘中获取字符串,然后把字符串存储到这个数组中。
fgets(buf, sizeof(buf), stdin); //stdin就是键盘设备对应的文件指针
//4.3 发送该字符串给服务器
send(sockfd, buf, strlen(buf), 0); //strlen(buf)可以计算出字符串实际的字符个数
//4.4 判断发送的字符串是不是quit
if( strncmp(buf, "quit", 4) == 0 )
{
break; //如果收到了quit,则跳出循环,程序结束
}
}
//5. 挂断电话
close(sockfd);
return 0;
}
三、结果显示:
四、网络编程通用API汇总
1. 创建未连接套接字:
头文件:
#include <sys/types.h>
#include <sys/socket.h>
定义函数:
int socket(int domain, int type, int protocol);
参数:
domain:域。
AF_INET/PF_INET:网际协议
AF_UNIX/PF_UNIX:本地协议,可写成AF_LOCAL/PF_LOCAL
type:类型。
SOCK_STREAM:流式套接字(TCP)
SOCK_DGRAM:数据报套接字(UDP)
protocol:协议。
一般为0
返回值:
成功:待连接套接字
失败:-1
备注:在网际协议中,选择流式套接字就代表TCP协议,选择数据包套接字就代表UDP协议,第三个参数protocol一般都不用。
======================================================
2、绑定套接字与网络地址
定义函数:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:待连接套接字
addr:包含本地地址(IP+PORT)的通用地址结构体的指针
addrlen:地址结构体大小
返回值:
成功:0
失败:-1
备注:
通用地址结构体的定义:
struct sockaddr
{
sa_family_t sa_family; -> 协议 -> 2
char sa_data[14]; -> IP、端口号.. -> 14
};
特殊地址结构体 —— IPv4地址结构体:
struct sockaddr_in
{
u_short sin_family; // 协议 -> 2
u_short sin_port; // 端口号 -> 2
struct in_addr sin_addr; // IPV4地址 -> 4
char sin_zero[8];
};
struct in_addr
{
in_addr_t s_addr; // 无符号32位网络地址 -> IP
};
/* Address to accept any incoming messages. */
#define INADDR_ANY ((unsigned long int) 0x00000000)
3.将待连接套接字设置为监听套接字,并设置最大同时接收连接请求个数
定义函数:
int listen(int sockfd, int backlog);
参数:
sockfd:待连接套接字
backlog:最大同时接收连接请求个数
返回值:
成功:0,并将sockfd设置为监听套接字
失败:-1
备注:
由于历史原因,各种系统对backlog的理解并不一致,以LINUX为例,监听端能同时接收的最大连接请求个数为backlog+4
======================================================
4、等待对端连接请求
定义函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:监听套接字
addr:通用地址结构体,用以存储对端地址(IP+PORT)
addrlen:参数addr的存储区域大小
返回值:
成功:已连接套接字(非负整数)
失败:-1
======================================================
5、连接对端监听套接字
接口声明:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:待连接套接字
addr:包含对端地址(IP+PORT)的通用地址结构体的指针
addrlen:地址结构体大小
返回值:
成功:0
失败:-1
======================================================
6. 断开本端连接套接字
头文件:
#include <unistd.h>
接口声明:int close(int fd);
参数:
fd:已连接套接字
返回值:
成功:0
失败:-1
备注:
同时断开读端和写端
======================================================
7、将文本地址转化为二进制地址
接口声明:
int inet_pton(int af, const char *src, void *dst);
参数:
af:地址族。AF_INET:IPv4地址
src:指向“点分式”IPv4或IPv6地址的指针,例如“192.168.1.100”
dst:类型为struct in_addr *的指针
返回值:
成功:1
失败:0代表地址与地址族不匹配,-1代表地址不合法
8、向TCP套接字发送数据
接口声明:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd:已连接套接字
buf:即将被发送的数据
len:数据长度
flags:发送标志,置为0即可以。
返回值:
成功:已发送字节数
失败:-1
9、从TCP套接字接收数据
接口声明:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:已连接套接字
buf:存储数据缓冲区
len:缓冲区大小
flags:接收标志,置为0即可以。
返回值:
成功:已接收字节数
失败:-1
10、字节序转换
头文件:
#include <arpa/inet.h>
接口声明:
uint32_t htonl(uint32_t hostlong); //h -> host 主机(本地) n ->net 网络
uint16_t htons(uint16_t hostshort); //l -> long 长4 s -> short 短2
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
参数:
hostlong: 主机字节序的长整型数据
hostshort: 主机字节序的短整型数据
netlong: 网络字节序的长整型数据
netshort: 网络字节序的短整型数据
返回值:
对应的字节序数据
11、字符串转整型
#include <stdlib.h>
atoi()