文章目录:
一:定义和流程分析
1.定义
2.流程分析
3.网络字节序
二:相关函数
IP地址转换函数inet_pton inet_ntop(本地字节序 网络字节序)
socket函数(创建一个套接字)
bind函数(给socket绑定一个服务器地址结构(IP+port))
listen函数(设置最大连接数或者说能同时进行三次握手的最大连接数监听上限)
accept函数(阻塞监听等待客户端建立连接, 成功的话返回一个与客户端成功连接的socket文件描述符)
connect函数(使用现有的socket与服务器建立连接)
三:服务器模型和客户端模型的实现
Server服务器的实现
Client客户端的实现
一:定义和流程分析
1.定义
定义:一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)
在通信过程中, 套接字一定是成对出现的
一种文件类型,伪文件,不占用存储空间,可进行IO操作,可间接看做文件描述符使
Socket本身有“插座”的意思
在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件
管道, 套接字, 块设备, 字符设备;
套接字: 一个fd可以索引读写两个缓冲区;
套接字的作用
套接字是网络通信中的一种端点,它提供了应用层进程利用网络协议交换数据的机制
套接字可以用于不同主机上的应用进程之间进行双向通信,使得它们可以交换数据、同步连接状态、处理错误等
在计算机系统中,套接字通常被实现为文件描述符,用于在网络上进行数据传输
当应用程序打开一个套接字时,操作系统会为它分配一个唯一的文件描述符,以便于进程间通信
2.流程分析
socket():创建一个套接字, 用fd索引
bind():绑定IP和port
listen():设置监听上限(同时与Server建立连接数)
accpet():阻塞监听客户端连接(传入一个上面创建的套接字, 传出一个连接的套接字)
在客户端中的connect()中绑定IP和port,并建立连接(阻塞)
3.网络字节序
小端法:(pc本地存储) 高位存高地址。低位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。低位存高地址。
htonl --> 本地--》 网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》 网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
用库函数做网络字节序和主机字节序的转换
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //主要针对IP
uint16_t htons(uint16_t hostshort); //主要针对port
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
二:相关函数
IP地址转换函数inet_pton inet_ntop(本地字节序 网络字节序)
由于如192.168.45.2
这种的IP地址为点分十进制表示,需要转化为uint32_t
型,有现成的函数(IPv4和IPv6都可以转换)
//本地字节序(string IP) ---> 网络字节序
int inet_pton(int af, const char *src, void *dst);
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
//网络字节序 ---> 本地字节序(string IP)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst、失败:NULL
socket函数(创建一个套接字)
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 创建一个 套接字
domain指定使用的协议(IPv4或IPv6)
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type指定数据传输协议(流式或报式)
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
指定代表协议(一般默认传0)protocol: 0
流式以TCP为代表;
报式以UDP为代表;
返回值:返回指向新创建的socket的文件描述符
成功:返回新套接字所对应文件描述符fd
失败:返回-1并设置errno;
bind函数(给socket绑定一个服务器地址结构(IP+port))
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)
sockfd: socket文件描述符
struct sockaddr_in servaddr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 构造出IP地址加端口号
传入参数(struct sockaddr *)&addr
addrlen: sizeof(addr) 地址结构的大小
返回值:
成功:0
失败:返回-1, 设置errno
listen函数(设置最大连接数或者说能同时进行三次握手的最大连接数监听上限)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog); //设置同时与服务器建立连接的上限数(同时进行3次握手的客户端数量)
sockfd:
socket文件描述符
backlog:上限数值。最大值 128
排队建立3次握手队列和刚刚建立3次握手队列的链接数和
返回值:
成功:0
失败:-1 errno
accept函数(阻塞监听等待客户端建立连接, 成功的话返回一个与客户端成功连接的socket文件描述符)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:
socket文件描述符
addr:成功与Sever建立连接的那个**客户端**的地址结构;
传出参数,返回链接客户端地址信息(IP地址+端口号)
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
socklen_t clit_addr_len=sizeof(addr);
入: 传入addr的大小;
出: 客户端addr的实际大小;
返回值:
成功: 返回能与客户端进行通信的socket对应的文件描述符;
失败: 返回-1并设置errno;
//我们的服务器程序结构是这样的
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
connect函数(使用现有的socket与服务器建立连接)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:socket文件描述符
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,服务器地址结构的长度sizeof(addr)大小
返回值:
成功返回0;
失败返回-1并设置errno;
如果不使用`bind()`函数绑定客户端的地址结构, 会采用**"隐式绑定"**;
三:服务器模型和客户端模型的实现
Server服务器的实现
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();
代码逻辑
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#define SERV_PORT 9527 //端口号
int main(int argc, char *argv[]){
int link_fd=0; //建立连接的socket文件描述符
int connect_fd=0 //用于通信的文件描述符
int ret=0; //用于检查是否出错
char buf[BUFSIZ]; //缓冲区
char client_IP[1024] //存入客户端IP字符串
int num=0; //读出的字节数
/*服务器端地址结构*/
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
/*成功与服务器建立连接的客户端地址结构*/
struct sockaddr_in clint_addr;
socklen_t clint_addr_len=sizeof(clint_addr); // 获取客户端地址结构大小
/*1.socket函数:创建用于建立连接的socket,返回的文件描述符存入link_fd*/
//IPv4,按照顺序基于字节流的连接,指定代表协议
link_fd=socket(AF_INET,SOCK_STREAM,0);
if(link_fd==-1)
sys_err("socket error");
/*2.bind函数:绑定服务器端的socket绑定地址结构(IP+port)*/
//socket文件描述符link_fd,IP地址加端口号
ret=bind(link_fd,(const struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret==-1)
sys_err("bind error");
/*3.listen函数:设定监听(连接)上线*/
ret=listen(link_fd,128);
if(ret==-1)
sys_err("listen error");
/*4.accept函数:阻塞等待客户端建立连接*/
//文件描述符,与Sever建立连接的客户端的地址结构,返回真正接收到地址结构体的大小
connect_fd=accept(link_fd,( struct sockaddr*)&clint_addr,&clint_addr_len);
if(connect_fd==-1)
sys_err("accept error");
/*建立连接后打印客户端的IP和端口号 获取客户端地址结构*/
printf(
"client IP:%s,client port:%d", //`client_IP`是前面定义的客户端IP字符串的缓冲区, 大小为1024
inet_ntop(AF_INET,&clint_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)), //网络字节序 ---> 本地字节序
ntohs(clint_addr.sin_port) //根据accept传出参数,获取客户端 ip 和 port
);
/*业务逻辑*/
while(1){
//5. read(fd) 读socket获取客户端数据
num=read(connect_fd,buf,sizeof(buf)); // 读客户端数据
write(STDOUT_FILENO,buf,num); // 写到屏幕查看
//6. 小--大写 toupper()
for(i=0;i<num;i++) // 小写 -- 大写
buf[i]=toupper(buf[i]);
//7. write(fd)
write(connect_fd,buf,num); // 将大写,写回给客户端
sleep(1);
}
//8. close()
close(connect_fd);
close(link_fd);
return 0;
}
测试命令
`nc 127.0.0.1 9527` //脑残命令: 向这个服务发送信息并打印回执
Client客户端的实现
client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据
5. 显示读取结果
6. close()
代码逻辑
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#define SERV_PORT 9527
/*错误处理函数*/
void sys_err(const char* str){
perror(str);
exit(1);
}
int main(int argc, char *argv[])){
int client_fd=0;
int ret=0;
int num=0;
int cnt=10;
char buf[BUFSIZ];
//connect的参数2填入服务器的文件描述符!
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);
// 本地字节序(string IP) ---> 网络字节序
inet_pton(AF_INET,"127.0.0.1",(void*)&serv_addr.sin_addr.s_addr);
/*1. 创建socket():客户端直接创建用于连接的套接字即可*/
client_fd=socket(AF_INET,SOCK_STREAM,0);
if(client_fd==-1)
sys_err("socket error");
/*2. connect():将客户端套接字与服务器地址结构连接起来*/
ret=connect(client_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret!=0)
sys_err("connect error");
//业务逻辑
while(--cnt){
//3. write() 写数据到 socket
write(client_fd,"fuckyou\n",8);
//4. read() 读转换后的数据。
num=read(client_fd,buf,sizeof(buf));
//5. 显示读取结果
write(STDOUT_FILENO,buf,num);
sleep(1);
}
//6. close()
close(client_fd);
return 0;
}