copyright:weishusheng data:2015.5.26
摘要:socket又叫套接字或者插口,它也是进程间通信的一种方式,实际上就是网络上的通信节点,应用程序只需要链接到socket就可以和网络上任何一个通信端点连接、传送数据。socket封装了通信的细节,我们可以不必关心通信协议内容而专注于应用程序开发。根据数据传送方式,socket分为面向连接的数据流通信和无连接的数据报通信。
1.创建socket
使用socket()函数创建socket对象,函数定义如下:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数参数详解:
domain指定使用的域,通常取值为AF_INET和AF_INET6,AF_INET表示使用IPv4协议,AF_INET6表示使用IPv6协议。type指定数据传输方式,type取值SOCK_STREAM表示面向连接的数据流方式,type取值SOCK_DGRAM表示无连接的数据报方式,type取值SOCK_RAW表示原始模式。protocol一般取0。socket()成功返回创建的函数句柄值。
2.面向连接的socket通信实现
图1 面向连接的socket数据流通信
所有的面向连接socket数据流通信都遵循这个过程。
2.1服务器端工作流程:
(1)使用socket()创建socket
(2)使用bind()把创建的socket绑定到指定TCP端口
(3)调用listen()使socket处于监听状态
(4)客户端发送请求后,调用accept()接受客户端请求,建立连接
(5)与客户端发送或接收数据
(6)通信完毕,关闭socket
2.2客户端工作流程:
(1)使用socket()创建socket
(2)调用connect()向服务器端socket发起连接
(3)建立连接后,进行数据读写
(4)通信完毕,关闭socket
2.3通信过程使用了不同的函数,下面分别进行介绍
2.3.1 bind()函数
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
sockfd是要绑定的socket句柄,my_addr指向一个sockaddr结构,里面保存IP地址和端口号,addrlen是sockaddr结构的大小。bind()如果绑定TCP端口成功,返回0,失败返回-1。
2.3.2 listen()函数
#include <sys/socket.h>
int listent(int s, int backlog);
s 是要监听的socket句柄,backlog指定最多可以监听的链接数量,默认是20个。如果listen调用成功,返回0,失败就返回-1.
2.3.3 accept()函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t addrlen);
accept函数用于面向连接的套接字类型。accept()将从连接请求队列中获得连接信息,创建新的套接字,并返回该套接字的描述符。accept返回的是一个新套接字描述符,客户端可以通过这个描述符和服务器通信,而最初通过socket创建的套接字描述符依然用于监听客户端请求。
参数s是监听的套接字描述符,参数addr是sockaddr结构的指针,addrlen是结构的大小。如果accept()调用成功,返回创建的套接字句柄,失败返回-1,并设置全局变量为errno。
2.3.4 connect()函数
客户端创建套接字后就可以用connect连接服务器。
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
connect()用于和服务器建立连接。sockfd是套接字句柄,serv_addr指向sockaddr结构,指定了服务器IP和端口;参数addlen是serv_addr结构大小。
2.3.5 客户端和服务器可以使用相同的发送和接受数据的函数,read()和write()就不再讲了;下面讲一下send()和recv()函数。
(1)发送函数send()
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void *buf, size_t len, int flags);
参数s是套接字描述符,buf是要发送的数据缓冲,len是数据缓冲长度,flags一般置0;send如果发送成功返回发送的字节数,失败返回-1.
(2)接收函数recv()
#include <sys/types.h>
#include <sys/socket.h>
int recv(int s, void *buf, size_t len, int flags);
参数s指定要读取的套接字句柄,buf是存放数据的缓冲首地址,len指定接收缓冲大小,flags一般置0;recv读取到数据时返回读取到的字节数,失败返回-1.另外,如果对方关闭了套接字,recv返回0;
3.面向连接的套接字实例
下面将以一个echo实例来说明socket通信,其中echo_serv.c是服务器端代码,当收到客户端发送的字符就在屏幕上打印出来,并把字符串发送给客户端,如果客户端发送quit就结束。
(1)服务器端程序echo_serv.c
#include <string.h>
#define ECHO_PORT 8080
#define MAX_CLIENT_NUM 10
int main()
{
int sock_fd;
struct sockaddr_in serv_addr;
int clientfd;
struct sockaddr_in clientAdd;
char buff[101];
socklen_t len;
int n;
/* creat socket */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if( sock_fd == -1)
{
perror("creat socket error!");
return 0;
}
else
{
printf("success to creat socket %d\n", sock_fd);
}
/*设置server地址结构*/
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(ECHO_PORT);
serv_addr.sin_addr.s_addr = htons(INADDR_ANY); //表示监听所有客户端ip,如需指定监听指定的客户端ip,可在此处指定,也可指定一个范围
bzero(&(serv_addr.sin_zero), 8);
/*把地址和套接字绑定*/
if(bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0)
{
printf("bind address fail! %d\n",errno);
close(sock_fd);
return 0;
}
else
{
printf("success to bind address! \n");
}
/*设置套接字监听*/
if(listen(sock_fd, MAX_CLIENT_NUM) != 0)
{
perror("listen socket error!");
close(sock_fd);
return 0;
}
else
{
printf("success to listen!\n");
}
/*创建新链接对应的套接字*/
len = sizeof(clientAdd);
clientfd = accept(sock_fd, (struct sockaddr*)&clientAdd, &len);
if(clientfd <= 0)
{
perror("accept() error!\n");
close(sock_fd);
return 0;
}
/*接收用户发来的数据*/
while((n = recv(clientfd, buff, 100, 0)) > 0)
{
buff[n] = '\n';
printf("number of receive bytes = %d data = %s\n",n, buff);
fflush(stdout);
send(clientfd, buff, n, 0);
if(strncmp(buff,"quit",4) == 0)
break;
}
close(clientfd);
close(sock_fd);
return 0;
}
INADDR_ANY选项
网络编程中常用到bind函数,需要绑定IP地址,这时可以设置INADDR_ANY.
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。
也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某
个端口,那他要监听哪个网卡地址的端口呢?如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网
卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个
套接字进行数据交换,这样岂不是很繁琐?
所以你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,
只要是绑定的端口号过来的数据,都可以接收到。
当然, 客户端connect时,不能使用INADDR_ANY选项。必须指明要连接哪个服务器IP。
(2)下面是echo客户端程序echo_client.c
#define ECHO_PORT 8080
#define MAX_COMMAND 5
int main()
{
int sock_fd;
struct sockaddr_in serv_addr;
char *buff[MAX_COMMAND] = {"abc", "def", "test", "hello", "quit"};
char tmp_buf[100];
int n,i;
/*creat socke*/
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if( sock_fd == -1)
{
perror("creat socket error!");
return 0;
}
else
{
printf("success to creat socket %d\n", sock_fd);
}
/*设置server地址结构*/
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(ECHO_PORT);
serv_addr.sin_addr.s_addr = htons(INADDR_ANY);
bzero(&(serv_addr.sin_zero), 8);
/*链接到服务器*/
if(-1 == connect(sock_fd, (struct sockaddr*)&serv_addr,sizeof(serv_addr)))
{
printf("connect error!\n");
close(sock_fd);
return 0;
}
printf("success connect to server!\n");
/*发送并接收缓冲的数据*/
for(i = 0; i < MAX_COMMAND; i++)
{
send(sock_fd, buff[i], 100, 0);
n = recv(sock_fd, tmp_buf, 100, 0);
tmp_buf[n] = '\n';
printf("data send:%s receive %s\n",buff[i], tmp_buf);
if(0 == strncmp(tmp_buf, "quit", 4))
break;
}
close(sock_fd);
return 0;
}
编译客户端和服务器端程序
[weishusheng@centOS6 echo_sock]$ gcc echo_client.c -o echo_client
[weishusheng@centOS6 echo_sock]$ gcc echo_serv.c -o echo_serv
运行服务器,输出:
[weishusheng@centOS6 echo_sock]$ ./echo_serv
success to creat socket 3
success to bind address!
success to listen!
number of receive bytes = 100 data = abc
number of receive bytes = 100 data = def
number of receive bytes = 100 data = test
number of receive bytes = 100 data = hello
number of receive bytes = 100 data = quit
在另一终端运行客户端,输出:
[weishusheng@centOS6 echo_sock]$ ./echo_client
success to creat socket 3
success connect to server!
data send: receive abc
data send:def receive def
data send:test receive test
data send:hello receive hello
data send:quit receive quit
4. socket通信十分重要,应用广泛,它还有一种通信方式,即无连接的socket通信
无连接的socket通信比较简单,它使用UDP协议,不保证数据能否到达,用在数据要求不高的地方,如在线视频。无连接的socket通信不需要建立连接,省去了维护连接的开销,所以速率更快。
无连接的socket数据报通信流程如图2
图2 无连接的socket数据报通信
和面向连接的数据流通信不同,无连接的socket数据报通信在服务器绑定socket到指定IP和端口后,没有调用listen()函数进行监听;也没有调用accept()函数对每个新的请求建立连接,因为没有连接的概念,传输层无法区分不同的连接,也就不需要对每个新的请求建立连接,在客户端创建socket后可以直接向服务器发送数据或者读取数据。
无连接的socket数据报通信发送和接收数据的函数和面向连接的套接字通信有点不同,分别使用recvfrom()和sendto()函数进行数据的发送和接收。它们的定义如下:
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
int sendto(int s, const void *msg, size_t len, int flags, struct sockaddr *to, socklen_t *tolen);
recvfrom用来从指定的IP和端口接收数据,s是socket句柄,buf是存放接收数据的缓冲首地址,len是接收缓冲大小,from是发送数据方的IP和端口号,fromlen是sockaddr结构大小。如果接收到数据,recvfrom返回接收到的字节数,失败返回-1;
sendto发送数据到指定的IP和端口,s指定socket句柄,msg是发送的数据的缓冲首地址,len是缓冲大小,to指定接收数据的IP和端口号,tolen是sockaddr结构大小。sendto()如果调用成功返回发送的字节数,失败返回-1。
无连接的时间服务通信实例
该例子服务器负责创建socket,绑定IP和端口,然后等待客户端发出请求当收到客户端的请求“time”后,生成当前时间发送给客户端。客户端创建socket后,直接向服务器发送请求时间命令,之后等待服务器返回,发送退出命令,关闭连接。
(1)服务器端,time_serv.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#define TIME_PORT 9090
#define DATA_SIZE 256
int main()
{
int sock_fd;
struct sockaddr_in local;
struct sockaddr_in from;
int n;
socklen_t fromlen;
char buff[DATA_SIZE];
time_t cur_time;
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if( sock_fd <= 0)
{
perror("creat socket error!");
return 0;
}
perror("Creat socket");
/*设置server地址结构*/
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(TIME_PORT);
local.sin_addr.s_addr = htons(INADDR_ANY);
//local.sin_addr.s_addr = inet_addr("192.168.1.21");
bzero(&(local.sin_zero), 8);
if(0 != bind(sock_fd, (struct sockaddr*)&local, sizeof(local)))
{
perror("bind address fail!\n");
close(sock_fd);
return 0;
}
printf("bind socket!");
fromlen = sizeof(from);
printf("waiting request from client...\n");
while(1)
{
n = recvfrom(sock_fd, buff, sizeof(buff), 0, (struct sockaddr*)&from, &fromlen);
if(n <= 0)
{
perror("recv data!\n");
lose(sock_fd);
return 0;
}
buff[n]='\n';
printf("client request:%s\n", buff);
if(0 == strncmp(buff, "quit", 4))
break;
if(0 == strncmp(buff, "time", 4))
{
cur_time = time(NULL);
printf("w1\n");
// strcpy(buff, asctime(gmtime(&cur_time)));
strcpy(buff, "weishusheng");
printf("now time is:%s",buff);
sendto(sock_fd, buff, sizeof(buff), 0, (struct sockaddr*)&from, fromlen);
printf("w2\n");
}
}
close(sock_fd);
return 0;
}
(2)客户端程序time_client.c
#define TIME_PORT 9090
#define DATA_SIZE 256
int main()
{
int sock_fd;
struct sockaddr_in serv;
int n;
socklen_t servlen;
char buff[DATA_SIZE];
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if( sock_fd <= 0)
{
perror("creat socket error!");
return 0;
}
perror("Creat socket");
bzero(&serv, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(TIME_PORT);
serv.sin_addr.s_addr = htons(INADDR_ANY);
//serv.sin_addr.s_addr = inet_addr("192.168.1.21");
bzero(&(serv.sin_zero), 8);
servlen = sizeof(serv);
strcpy(buff, "time");
if(-1 == sendto(sock_fd, buff, sizeof(buff), 0, (struct sockaddr*)&serv, servlen))
{
perror("send data!");
close(sock_fd);
return 0;
}
printf("send time request\n");
n = recvfrom(sock_fd, buff, sizeof(buff), 0, (struct sockaddr*)&serv, &servlen);
printf("program goes to recvfrom()\n");
if(n <= 0)
{
perror("recv data!\n");
close(sock_fd);
return 0;
}
buff[n]='\n';
printf("time from server:%s\n", buff);
strcpy(buff,"quit");
if(-1 == sendto(sock_fd, buff, sizeof(buff),0 , (struct sockaddr*)&serv, servlen))
{
perror("send data!");
close(sock_fd);
return 0;
}
printf("send quit command\n");
close(sock_fd);
return 0;
}
编译运行即可看到客户端和服务器的通信过程。