网络通信,首先那些七层模型等概念,小的不才,之前有写过几篇关于网络的文章,如果有时间,可以去看看,浅显易懂。可能写的不好,但这里只是个人的一些见解吧。
Socket本身有“插座”的意思,在Unix/Linux环境下,用于表示进程间网络通信的特殊文件类型(Linux下一切皆文件)。本质为内核借助缓冲区形成的伪文件。那么就可以使用文件描述符引用套接字, 与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,"IP地址+TCP或UDP端口号"唯一标识网络通讯中的一个进程。"IP地址+端口号"就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。(左边是server端的套接字,右边是client端的套接字)
我觉得对于Socket通信,网上的很多文章没有讲到一个很重要的点。对于一个Server和一个Client端单线通信,Server端会有两个套接字,而Client端只有一个。为什么Server端会有两个呢?Server端其中一个就是用来和Client端通信的。另一个是用来监听的,以防会有其他Client端想与Server端创建连接。
简单的来说:
在Server端,socket()返回的套接字(lfd)用于监听(listen)和接受(accept)Client端的连接请求。这个套接字不能用于与Client端之间发送和接收数据。当连接成功后,会返回一个新的套接字(cfd),Server端是用这个新的套接字与Client端通信。而之前的那个套接字(lfd),会接着去与其他Client端进行连接,连接后再返回一个新的套接字…
不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。TCP/IP协议规定,网络字节序统一为大端序。
主机A先把数据转换成大端序再进行网络传输,主机B收到数据后先转换为自己的格式再解析。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include
/*主机字节顺序 --> 网络字节顺序*/
uint32_t htonl(uint32_t hostlong); /*转32位,IP是32位,所以是针对的是IP*/
uint16_t htons(uint16_t hostshort);/*转16位,针对的是端口*/
/*网络字节顺序 --> 主机字节顺序*/
uint32_t ntohl(uint32_t netlong); /*IP*/
uint16_t ntohs(uint16_t netshort); /*端口*/
对于Server端,可以使用INADDR_ANY宏,代表从本地取一个有效的无符号整数的IP地址,一步到位。
而对于客户端,绑定Server端的IP,要从String->int->htonl 转换,很麻烦,所以,就有了下面两个网络地址转换函数:
#include
/*点分十进制的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
*/
/*网络字节序转换为点分十进制的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
*/
可以使用 [ man 7 ip ]命令查看
还要了解sockaddr数据结构, const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同使用不同结构体。原来的结构体即左边第一个, 这种使用不方便,所以出现针对不同类型协议的结构体,可以直接访问其对应的值,主要是IP和port(端口)。
ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address IP*/
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
查看帮助:man socket
原型:int socket(int domain, int type, int protocol)
函数作用:为通讯创建一个套接字,返回该套接字的文件描述符
函数返回值:成功,返回新套接字所对应的文件描述符 。失败返回-1
参数:
查看帮助:man 2 bind
原型: int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数作用:给Socket绑定一个地址结构(IP+端口号),地址结构在sockaddr_in这个结构体中。
函数返回值:成功返回0,失败返回-1
参数:
查看帮助:man listen
原型:int listen(int sockfd, int backlog);
函数作用:设置同时与服务器建立连接的上限数
函数返回值:函数成功返回0,失败返回-1
参数:
查看帮助:man 2 accept
原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数作用:阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接Socket文件描述符
函数返回值:成功,返回能与服务器进行数据通信的Socket对应的文件描述符;
出错返回-1
查看帮助:man 2 accept
原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数作用:使用现有的Socket与服务器建立连接
函数返回值:
代码作用:一个客户端与一个服务端建立连接,客户端输入小写字母,传输到服务端。服务端打印客户端的IP和端口号,再把收到的小写字母转换为大写字母,再传给客户端。
/***
Server端
Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 10005
#define BUFIZE 4096
void sys_err(const char *str){
perror(str);
exit(-1);
}
int main(int argc,char *argv[] ){
int lfd=0,cfd=0;
int ret,i;
char buf[BUFIZE];
char client_IP[BUFIZE];
struct sockaddr_in serv_addr,clt_addr;
socklen_t clt_addr_len; //客户端地址结构长度
//服务端建立一个用来监听的Socket套接字lfd
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0)
sys_err("socket error");
bzero(&serv_addr,sizeof(serv_addr)); //清空服务端的地址结构
//设置服务端的地址结构
serv_addr.sin_family=AF_INET; //绑定的协议
serv_addr.sin_port=htons(SERV_PORT); //绑定的端口号
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); //绑定的IP
//INADDR_ANY宏,代表从本地取一个有效的IP地址
//绑定地址结构
if(bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))==-1)
sys_err("Bind error.\n");
//设置监听上限
if(listen(lfd,10)==-1)
sys_err("Listen error");
else
printf("Start to listen!\n");
clt_addr_len=sizeof(clt_addr);
//阻塞等待客户端建立连接,建立连接成功,返回一个专用套接字cfd,用来与client端通信
cfd=accept(lfd,(struct sockaddr *)&clt_addr,&clt_addr_len);
if(cfd==-1)
sys_err("accept error");
//打印客户端的IP、端口号
printf("client ip is:%s client port:%d\n",inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),ntohs(clt_addr.sin_port));
//循环的读缓冲区内的内容
while(1){
ret = read(cfd,buf,sizeof(buf));
//检测到客户端关闭
if (ret==0){
printf("client has been drop out.\n");
close(cfd);
exit(-1);
}
//小写转大写
for(i=0;i<ret;i++)
buf[i]=toupper(buf[i]);
printf("What to send to the client:\n");
//写回客户端
write(cfd,buf,ret);
//为了验证转换是否正确,将转换后的结果输出在server端的屏幕上,这步可以不需要~~
write(STDOUT_FILENO,buf,ret);
printf("\n");
}
close(lfd); //关闭套接字lfd
close(cfd); //关闭套接字cfd
return 0;
}
/***
client端
Author:Liang jie
objective:服务端将客户端输入的小写转换为大写,再传回客户端。
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 10005
#define CONNECT_NUM 5
#define BUFIZE 4096
#define MAX_NUM 80
void sys_err(const char *str){
perror(str);
exit(-1);
}
int main(void){
int cfd;
struct sockaddr_in serv_addr; //服务器地址结构
int ret;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons((u_short) SERV_PORT);
//网络字节序转本地字节序
inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr); //绑定的IP应该是服务器的IP
cfd = socket(AF_INET,SOCK_STREAM,0);
if(cfd<0)
sys_err("socket error");
if(connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr))<0)
sys_err("connect errpr");
printf("Connect successful.\n");
char sedBuf[MAX_NUM]={
0}; //发送缓冲区
char revBuf[MAX_NUM]={
0}; //接收缓冲区
//只要客户端不关闭,就一直循环
while(gets(sedBuf)!=-1){
//阻塞写,将客户端写的内容cpoy到发送缓冲区中,如果不写,就一直阻塞在这
if(write(cfd,sedBuf,strlen(sedBuf))<0)
sys_err("write error");
bzero(sedBuf,sizeof(sedBuf)); //清空缓冲区
//从套接字cfd中的接收缓冲区中,读服务端发来的内容
if(read(cfd,revBuf,sizeof(revBuf))<0)
sys_err("read error");
else
printf("Sever:%s\n",revBuf); //打印
bzero(revBuf,sizeof(revBuf)); //清空缓冲区
}
close(cfd);
return 0;
}
文章很浅显的讲解了Linux下的Socket编程,例子很简单,只是为了能够更好的了解Socket编程。下面几篇文章,是关于多线程、多进程,阻塞、非阻塞的Socket编程,包括聊天室等。敬请关注!!