1.socket简介
在我们常用的网络通信中,socket是最常用的一种,socket编程也称套接字编程,下面我们将对socket编程进行相关讲解,其中包括服务器与客户端通信讲解,以及代码实现。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
2.socket网络通信步骤
在我们利用socket创建网络通信连接的时候,遵循下图所示的步骤:
如图1所示,当我们两台PC进行通信时,一方的服务器端server向另一方的客户端client发送数据时,由socket创建网络通道,但必须指定双方所使用的协议种类及端口号,同时还需告知IP地址。
其中IP地址相当于住址,用于查找PC所在的位置,端口号相当于“房间号”,用与告知通信双方所使用的端口,常用的通信协议由TCP/UDP两种,下面将补充介绍:
TCP/UDP协议对比:
1.TCP是面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
2.TCP提供可靠的服务。也就是说TCP连接传送的数据无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
3.TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低。
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互式通信
5.TCP首部开销20字节;UDP的首部开销小,只有8字节。
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
如图2所示,在服务器端,首先新建一个socket套接字,同时绑定地址及端口号,这时进入监听状态,客户端此时若想连接服务器端,则发出连接请求,待服务器端接收请求后,一条完整的通信方式就建立完成,客户端和服务器便可进行数据交换了。
3.相关API介绍
创建套接字int socket(int domain,int type,int protocol),其中domain指明所使用的协议族,通常使用的因特网域为IPv4,设置为AF_INET。type为参数指定socket的类型,我们使用的为TCP协议,可以保证数据传输的正确性与顺序性,因此第二个参数设置为SOCK_STREAM。potocol通常赋值为“0”,0选择type类型对应的默认协议。返回值:若成功则返回socket套接字的文件描述符,若失败则返回-1.
IP与端口号绑定int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen),参数说明:sockfd为socket返回的描述符,addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针。其中第二个参数addr给出的结构体如下:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
结构体中的成员,sa_data[ ]表示进程地址;而bind函数中的第三个参数addrlen表示参数addr的长度;addr参数可以接受多种类型的结构体,而这些结构体的长度各不相同,因此需要使用addrlen参数额外指定结构体长度;经常为了传输IPv4的地址及端口号,定义一个struct sockaddr_in类型的结构体,但最后使用bind函数时必须转换成struct sockaddr *类型的。struct sockaddr_in如下:
struct sockaddr_in{
sa_family_t sin_family;//协议族
in_port_t sin_port;//端口号
struct in_addr sin_addr;//IP地址结构体
unsigned char sin_zero[8]://填充,没有实际意义
}
注意,在本结构体的第三个成员,相当于结构体的成员为结构体,注意结构体成员的引用。
地址转换int inet_aton(const char *straddr,struct in_addr *addrp);该函数的作用是将字符串形式的IP地址如“192.168.0.0.1”转换成网络能识别的形式,第一个参数straddr为IP地址字符串,第二个参数为struct in_addr *型数据,因此保持类型一致,我们首先给struct sockaddr_in中的第三个成员结构体给出IP地址,此时的类型刚好为struct in_addr类型,但要求的是结构体指针,我们只需进行取地址运算即可。
cha *inet_ntoa(struct in_addr inaddr)该函数的作用是将网络格式的IP地址转换成字符串格式,通常用于接收方接收对方的IP地址并转换为字符串形式。
监听int listen(int sockfd,int backlog);该函数只用于服务器端不断等待客户端的连接请求,第一个参数为描述符,第二个参数backlog指定在请求队列中允许的最大请求数。
接收连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);该函数用于接受客户端请求的连接。第一个参数为描述符,第二个参数addr用来返回已连接的客户端的协议地址,第三个参数addrlen为客户端地址长度。
请求连接int connect(int sockfd,const struct sockaddr *addr,socklen_t *addrlen)该函数用于客户端请求连接服务器端的函数,sockfd为描述符,addr为服务器端的IP地址和端口号的地址结构指针,addrlen为地址长度。
服务器端代码如下:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
char msg[128]={0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1){
perror("socket");
exit(-1);
}
//2.bind
s_addr.sin_family=AF_INET;
s_addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
//4.accept
int c_len=sizeof(struct sockaddr_in);
while(1){
c_fd=accept(s_fd,(struct sockaddr *)&c_addr,&c_len);
if(c_fd==-1){
perror("accept");
}
printf("get connect :%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
if(fork() == 0){
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input \n:");
gets(msg);
write(c_fd,msg,strlen(msg));
sleep(1);
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read=read(c_fd,readBuf,128);
if(n_read==-1){
perror("read");
}
else{
printf("get message %d:%s\n ",n_read,readBuf);
}
}
}
}
return 0;
}
在服务器端,当我们与客户端连接成功后,我们开辟一个子进程用来不断地读取客户端发送过来的消息并打印,与此同时再开辟一个子进程用来检测用户的输入,实现可以将服务器端的消息发送给客户端。
客户端代码:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
char msg[128]={0};
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
c_fd=socket(AF_INET,SOCK_STREAM,0);
if(c_fd==-1){
perror("socket");
exit(-1);
}
//2.connect
c_addr.sin_family=AF_INET;
c_addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr))==-1){
perror("connect");
exit(-1);
}
printf("get connect :%s\n",inet_ntoa(c_addr.sin_addr));
while(1){
//3.send
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input \n:");
gets(msg);
write(c_fd,msg,strlen(msg));
sleep(2);
}
}
//4.read
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read=read(c_fd,readBuf,128);
if(n_read==-1){
perror("read");
}
else{
printf("get message %d:%s\n ",n_read,readBuf);
}
}
}
return 0;
}
与服务器端不同,当我们请求连接成功后,便开辟子进程与服务器间进行通信。