C端
1 获取socket socket()
2 给socket取得地址(可省略) bind() 绑定IP地址和端口
3 发送连接 connect()
4 收发消息 recv()
5 关闭连接 close()
S端
1 获取socket socket()
2 给socket取得地址 bind() 绑定IP地址和端口
3 将socket置为监听模式 lsiten()
4 接受连接 accept()
5 收发消息 send()
6 关闭连接 close()
listen() 监听一个SOCKET
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog原本是指 半连接池的大小,原本是指可以存放多少半连接状态的节点。但是由于因为半连接洪水的攻击,已经放弃了半连接池,所以 backlog 现在变成了指 S端能够承受建立全连接的C端的最大数量。
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
accept() 在SOCKET上建立连接
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
struct sockaddr *addr: 对端地址信息
socklen_t *addrlen: 对端地址长度
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
同样
struct sockaddr 依赖于当前用到的协议族中的地址信息 The sockaddr structure is defined as something like:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
不同的协议族 来 绑定自己这端的地址 所用的结构体是不一样的。所以是不存在 struct sockaddr 类型的。所以我们的处理方式是:我们用的是哪一个协议族,就把该协议族地址作为addr ,然后再把地址长度写到addrlen
AF_INET see ip(7)
man 7 ip
Address format
An IP socket address is defined as a combination of an IP interface address and a 16-bit port number. The basic IP protocol does not supply port numbers, they are implemented by higher level
protocols like udp(7) and tcp(7). On raw sockets sin_port is set to the IP protocol.
/*
注意 :IP地址和端口,是需跟着网络一起发送的。代表自己的身份
*/
struct sockaddr_in {
//协议族 address family: AF_INET
sa_family_t sin_family;
//需要的端口
in_port_t sin_port;
//IP地址 并非点分式,而是大整数internet address ,用的时候需要格式转换:inet_pton()
struct in_addr sin_addr;
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
所以 AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
RETURN VALUE
如果成功,这些系统调用将返回一个非负整数,它是所接受套接字的描述符。出现错误时,返回-1,并适当地设置errno。
实验:流式套接字
proto.h
#ifndef PROTO_H_
#define PROTO_H_
#define SERVERPORT "1989"
#define FMT_STAMP "%lld\r\n"
#endif
server.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
static void server_job(int sd)
{
char buf[BUFSIZE];
int len;
//将时间戳 以 FMT_STAMP格式 存储到 buf中
len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
if(send(sd,buf,len,0) < 0)
{
perror("send()");
exit(1);
}
}
int main()
{
int sd,newsd;
struct sockaddr_in laddr,raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
sd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);
if(sd < 0)
{
perror("soccket()");
exit(1);
}
//设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
laddr.sin_family = AF_INET; // 协议族为 AF_INET
// 设置端口为1989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons
laddr.sin_port = htons(atoi(SERVERPORT));
// 设置IP
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);//0.0.0.0:any address
// 给SOCKET绑定一个地址,关联到目标协议族地址信息结构体,即绑定IP,端口
//(void *)&laddr, 实际并没有struct sockaddr类型,所以暂时强转为 void *
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind");
exit(1);
}
//将socket置为监听模式, 承受C端最大数量为200
if(listen(sd,200) < 0)
{
perror("listen");
exit(1);
}
/* !!!! */
raddr_len = sizeof(raddr);
while(1)
{
//接受连接,保存对端地址信息,如果成功,将返回一个非负整数,它是所接受套接字的描述符,即newsd
//默认为阻塞等待连接
newsd = accept(sd,(void *)&raddr,&raddr_len);
if(newsd < 0)
{
perror("accept");
exit(1);
}
//将接收到的对端地址中IP信息 从大整数 转换为 点分式 保存到ipstr数组
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
//打印对端 IP:端口
//raddr.sin_port 是从socket接收到的信息,不是单字节信息,需要字节序转换 ntohs
printf("Client %s:%d\n",ipstr,ntohs(raddr.sin_port));
server_job(newsd);
close(newsd);
}
close(sd);
exit(0);
}
可以看到 1989端口已经打开,目前处于 Listen模式。
用标准客户端nc 工具自测,即 nc ip port,即
nc 127.0.0.1 1989 :请求向 127.0.0.1机器(即本机)1989端口 发送请求,可以看到有数据返回,当前时间戳
一个关键问题:
在 CTL+C 结束刚刚运行的 server端进程后,立刻再次运行 server端进程,会提示
Address already in use
再次 netstat -ant 后会显示 1989端口 处于 TIME_WAIT状态,表明该端口还在工作当中。问题就在于 之前 CTL+C 结束进程,用信号将进程终止,但是server端 最后的 close(sd) 并没有执行到,也就是没有正常结束,没有去刷新该刷新的流,也没有将当前的SOCKET 和 端口释放,所以紧随其后的再次运行 server进程会提示 端口busy,就是因为上一次结束没有正常释放端口, 而等一会儿就可以继续申请1989端口,这是因为内核发现了 1989端口 对应的 socket已经被异常终止了,内核就会帮忙释放掉 1989端口。这样就可以再次运行server服务了。
解决端口释放问题:
添加属性 : SO_REUSEADDR,表明再次 bind()的时候,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。
对指定Socket sd 的SOL_SOCKET层面 的 SO_REUSEADDR属性进行设置,即打开该属性,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,&val, sizeof(val)) < 0)
{
perror("setsockopt()");
exit(1);
}
所以server端最终代码应该加上 SO_REUSEADDR 属性
server.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
static void server_job(int sd)
{
char buf[BUFSIZE];
int len;
//将时间戳 以 FMT_STAMP格式 存储到 buf中
len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
if(send(sd,buf,len,0) < 0)
{
perror("send()");
exit(1);
}
}
int main()
{
int sd,newsd;
struct sockaddr_in laddr,raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
sd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);
if(sd < 0)
{
perror("soccket()");
exit(1);
}
//对指定Socket sd 的SOL_SOCKET层面 的 SO_REUSEADDR属性进行设置,即打开该属性,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,&val, sizeof(val)) < 0)
{
perror("setsockopt()");
exit(1);
}
//设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
laddr.sin_family = AF_INET; // 协议族为 AF_INET
// 设置端口为1989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons
laddr.sin_port = htons(atoi(SERVERPORT));
// 设置IP
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);//0.0.0.0:any address
// 给SOCKET绑定一个地址,关联到目标协议族地址信息结构体,即绑定IP,端口
//(void *)&laddr, 实际并没有struct sockaddr类型,所以暂时强转为 void *
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind");
exit(1);
}
//将socket置为监听模式, 承受C端最大数量为200
if(listen(sd,200) < 0)
{
perror("listen");
exit(1);
}
/* !!!! */
raddr_len = sizeof(raddr);
while(1)
{
//接受连接,保存对端地址信息,如果成功,将返回一个非负整数,它是所接受套接字的描述符,即newsd
//默认为阻塞等待连接
newsd = accept(sd,(void *)&raddr,&raddr_len);
if(newsd < 0)
{
perror("accept");
exit(1);
}
//将接收到的对端地址中IP信息 从大整数 转换为 点分式 保存到ipstr数组
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
//打印对端 IP:端口
//raddr.sin_port 是从socket接收到的信息,不是单字节信息,需要字节序转换 ntohs
printf("Client %s:%d\n",ipstr,ntohs(raddr.sin_port));
server_job(newsd);
close(newsd);
}
close(sd);
exit(0);
}
客户端:
connect() 建立连接 socket
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*
const struct sockaddr *addr:对端地址,同样,没有这样类型,我们此时使用的地址是struct sockaddr_in类型
socklen_t addrlen:对端地址长度
*/
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
client.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "proto.h"
int main(int argc,char *argv[])
{
int sd;
struct sockaddr_in raddr;
long long stamp;
FILE *fp;
if(argc < 2)
{
fprintf(stderr,"Usage...!\n");
exit(1);
}
//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0)
{
perror("soccket()");
exit(1);
}
//bind()
//设置对端地址信息,即设置S端地址信息
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,argv[1],&raddr.sin_addr);
//建立连接
if(connect(sd,(void *)&raddr,sizeof(raddr)) < 0)
{
perror("connect()");
exit(1);
}
/*
接下来就是接收数据,正常使用 recv()函数接收,这里用文件操作来解析接收的数据来加深 一切皆文件的思想,本身sd就是一个文件描述符
将给定文件描述符 并指定操作权限,转换为 FILE* 文件流的操作,转换成功之后,我们对当前socket的操作就完全转换为了标准IO 的操作,
这样所有可移植的标准库函数都可以使用,fread() fwrite() ...
*/
fp = fdopen(sd,"r+");
if(fp == NULL)
{
perror("fdopen()");
exit(1);
}
//从指定流fp中拿数据 以 format格式存储到 目标地址
if(fscanf(fp,FMT_STAMP,&stamp)<1)
fprintf(stderr,"Bad format!\n");
else
fprintf(stdout,"stamp = %lld\n", stamp);
//关闭fp
fclose(fp);
exit(0);
}
C端请求 127.0.0.1 上面的内容,成功返回当前时间戳。