c语言实现简单的hello/hi程序
使用tcp协议来实现来实现
实现效果
实现过程
对于服务器端:
1.定义sockadr_in
结构体
struct sockaddr_in add={
.sin_family=AF_INET,
.sin_port=htons(8000),
.sin_addr.s_addr=htonl(INADDR_ANY),
};
2.初始化:
sin_family表示协议簇,一般用AF_INET表示TCP/IP协议
sin_port端口为8000
sin_addr将本地所有的ip都绑定到地址
3.通过soket获取文件标识符sock_fd=socket(PF_INET,SOCK_STREAM,0)
4.将sockaddr_in结构体与sock_fd标识符绑定
bind(sock_fd,(struct sockaddr*)&add,sizeof(struct sockaddr)
5.监听端口,如果有链接就能响应
ret=listen(sock_fd,MAX_QUEUE_SIZE))
接收请求,同时获取请求方的套接字,通过这个套接字能进行数据的收发
accept(sock_fd,(struct sockaddr*)&addNew,&sin_size)
对于客户端
1.初始化套接字
struct sockaddr_in server;
memset(&server,0,sizeof(struct sockaddr_in ));
server.sin_family = AF_INET;
server.sin_port =8000;
server.sin_addr.s_addr = inet_addr("127.0.0.1");
与服务端的套接字不同,这里初始化的套接字表示服务端的套接字,即客户端要向谁请求服务,就应该将套接字初始化为对应的服务端。
这里定义端口为8000,因为服务端的端口设置为8000,ip地址为127.0.0.1,即回环地址,因为实验在一台主机上完成,服务端也布置在本主机,所以请求服务时的目的地址就是本机的地址。
2.获取文件描述符
sockfd = socket( AF_INET, SOCK_STREAM,0)
通过socket接口,定义服务类型为TCP服务,返回文件描述符,有了文件描述符与套接字,我们就能请求服务了。
3.连接服务端
connect( sockfd,(struct sockaddr*)&server,sizeof( server ))
connet函数传递了三个参数:文件描述符,服务端的套接字结构体,套接字的大小,这个函数会根据套接字尝试服务端。如果服务端此时已经执行过了listen,那么服务端就能相应这个请求,并进行三次握手,从此,服务端于客户端的连接就建立完成了。
4.发送与接收消息
send(client_sockfd,buf,len,0)
recv(client_sockfd,buf,BUFSIZ,0);
对于发送消息与接收消息,服务端与客户端没有区别,其实对于接收和发送消息,read与write操作也能实现。
由于已经建立了连接,所以发送和接收时并不需要目标机的套接字,只需要传递需要发送的数据或者接收数据的缓冲区,自己的文件描述符即可!
代码实现
客户端
#include
#include
#include
#include
#include
#include
#include
#define MAX_len 1024
int sock_fd;
struct sockaddr_in add;
int main()
{
int ret;
char buf[MAX_len]={0};
char buf_rec[MAX_len]={0};
char buf_p[5]={"0"};
memset(&add,0,sizeof(add));
add.sin_family=AF_INET;
add.sin_port=htons(8000);
add.sin_addr.s_addr=inet_addr("127.0.0.1");
if((sock_fd=socket(PF_INET,SOCK_STREAM,0))<=0)
{
perror("socket");
return 1;
}
if((ret=connect(sock_fd,(struct sockaddr*)& add,sizeof(struct sockaddr)))<0)
{
perror("connet");
return 1;
}
if((ret=send(sock_fd,(void*)buf_p,strlen(buf),0))<0)
{
perror("recvfrom");
return 1;
}
while (1)
{
scanf("%s",buf);
if((ret=send(sock_fd,(void*)buf,sizeof(buf),0))<0)
{
perror("sendfrom1");
return 1;
}
if((ret=recv(sock_fd,(void*)buf_rec,sizeof(buf_rec),0))<0)
{
perror("recvfrom1");
return 1;
}
printf("%s\n",buf_rec);
}
return 0;
}
服务端
#include
#include
#include
#include
#include
#include
#include
#define MAX_len 1024
#define MAX_QUEUE_SIZE 10
#define MAX_CON 10
void dealData(int cilient)
{
char buf[1024];
char buf_p[5]={"hi"};
int des_fd,len=0;
while( 1 ) {
memset(buf,0,sizeof(buf));
len=recv(cilient,(void*)buf,sizeof(buf),0);
send(cilient,buf_p,strlen(buf_p),0);
}
}
int main()
{
int len=0;
int sock_fd_work;
int cilient;
int sin_size=sizeof(struct sockaddr_in);
int sock_fd;
char buf_p[5];
char buf[100];
int ret,current=-1,des;
struct sockaddr_in addNew;
struct sockaddr_in add
={
.sin_family=AF_INET,
.sin_port=htons(8000),
.sin_addr.s_addr=htonl(INADDR_ANY),
};
if((sock_fd=socket(PF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
return 1;
}
if((ret=bind(sock_fd,(struct sockaddr*)&add,sizeof(struct sockaddr)))<0)
{
perror("bind");
return 1;
}
if((ret=listen(sock_fd,MAX_QUEUE_SIZE))<0)
{
perror("listen");
return 1;
}
while(1)
{
if((sock_fd_work=accept(sock_fd,(struct sockaddr*)&addNew,&sin_size))>0)
if(!fork())
{
dealData(sock_fd_work);
exit(0);
}
}
close(sock_fd_work);
return 0; /* code */
}
实现原理
跟踪分析
在linux中,socket也被看做是文件,也正是因为如此,对文件的read
和write
操作也能对socket
使用,进一步来讲,内核实现socket
与实现其他文件系统基本类似,也就是说,socket也是一个文件系统。
作为一个linux下的文件系统,我们关注的数据结构有:
超级块
struct super_block {
...
struct file_system_type *s_type;
struct super_operations *s_op;
...
}
显然,file_system_type为文件系统的类型,socket文件系统对应的内容为
static struct file_system_type sock_fs_type ={
.name ="sockfs",// 文件系统名称
.mount = sockfs_mount,// 挂载sockfs函数,其中会创建super block
.kill_sb = kill_anon_super,// 销毁super block函数
};
里面的内容其实很少,名称、挂载操作,还有销毁操作。
那么struct super_operations呢? 这里面保存了一些文件系统在创建或删除文件时会用到的操作
static const struct super_operations sockfs_ops ={
.alloc_inode = sock_alloc_inode,// 分配inode
.destroy_inode = sock_destroy_inode,// 释放inode
.statfs = simple_statfs,// 用于获取sockfs文件系统的状态信息
};
由于linux将socket也当作一个文件对待,那么在新建一个连接(执行socket())时,应该也会创建一个文件才对,一个磁盘文件对应了一个inode,也就是每执行一次socket,都会执行sock_alloc_inode操作,然后创建一个inode,然后创建一个file文件,将file文件指向那个inode,然后返回文件描述符。
验证
若执行socket(PF_INET,SOCK_STREAM,0),就会执行系统调用 sys_socketcall,实际上就是SYSCALL_DEFINE2
SYSCALL_DEFINE2(socketcall,int, call,unsignedlong __user *, args)
{
switch(call){
case SYS_SOCKET:
// 与 socket(int domain, int type, int protocol) 对应,创建socket
err = sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = sys_bind(a0,(struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0,(struct sockaddr __user *)a1, a[2]);
break;
}
从这里的代码可以看到,sys_socketcall
是几乎所有socket操作的入口(socket、bind、connect、listen),每次执行都会根据系统调用号SYS_SOCKET来判断用户程序的请求,进而执行对应的系统调用:sys_socket、sys_bind、sys_connet
。
sys_socket
:内核将sys_socket重定位为SYSCALL_DEFINE3
SYSCALL_DEFINE3(socket,int, family,int, type,int, protocol)
{
...
retval = sock_create(family, type, protocol,&sock);
....
retval = sock_map_fd(sock, flags &(O_CLOEXEC | O_NONBLOCK));
}
对于sock_create
内核会分配一个socket结构体,结构体内包括socket的操作,等待队列(监听会用到)等
struct socket {
socket_state state;// 连接状态:SS_CONNECTING, SS_CONNECTED 等
short type;// 类型:SOCK_STREAM, SOCK_DGRAM 等
unsignedlong flags;// 标志位:SOCK_ASYNC_NOSPACE(发送队列是否已满)等
struct socket_wq __rcu *wq;// 等待队列
struct file *file;// 该socket结构体对应VFS中的file指针
struct sock *sk;// socket网络层表示,真正处理网络协议的地方
conststruct proto_ops *ops;// socket操作函数集:bind, connect, accept 等
};
sock_create
的内容:
int __sock_create(struct net *net,int family,int type,int protocol,
struct socket **res,int kern)
{
...
err = security_socket_create(family, type, protocol, kern);
sock = sock_alloc();
...
}
security_socket_create
函数主要是是判断了传递的参数是否合法,在用户态创建还是内核空间创建,它的实现如下:
static struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
inode = new_inode_pseudo(sock_mnt->mnt_sb);
sock = SOCKET_I(inode);
....
}
new_inode_pseudo
调用了socket文件系统的分配节点操作alloc_inode
,最终实现了socket节点的创建
static struct inode *alloc_inode(struct super_block *sb)
{
if(sb->s_op->alloc_inode)
inode = sb->s_op->alloc_inode(sb);
...
}
这里的sb就是socket的超级块,调用的alloc_inode
,正是在初始化文件系统时赋值给超级块sockfs_ops
域的函数指针,这也部分验证了我们的猜想,那打开的文件呢?显然就是由系统调用中sock_map_fd
来完成。
对于sock_map_fd
,内核会创建一个文件,并将socket与文件绑定起来,同时会分配文件描述符,也就是socket的返回值,通过文件描述符,既能将socket看着文件来对待,又能执行socket特有的函数。
staticint sock_map_fd(struct socket *sock,int flags)
{
struct file *newfile;
// 从本进程的文件描述符表中获取一个可用的文件描述符
int fd = get_unused_fd_flags(flags);、
// 创建一个新的file,并将file和inode以及socket关联
newfile = sock_alloc_file(sock, flags, NULL);
}
这一步通过get_unused_fd_flags得到了文件描述符,sock_alloc_file负责创建对应的文件
struct file *sock_alloc_file(struct socket *sock,int
flags,constchar*dname)
{
struct file *file;
file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);
file->private_data = sock;
}
清晰的看到,文件被创建、得到文件指针、将socket文件操作赋值给file、将scket结构体sock赋值到file->private——data
。这一步完成,我们就能将socket看做一个文件了。
验证完毕
参考:https://blog.csdn.net/qq_14978113/article/details/80738787