1、套接字概述
套接字的本意是插座,在网络中用来描述计算机中不同程序与其他计算机程序的通信方式。
常用的套接字类型有3种:
1)流套接字(SOCK——STREAM):使用了面向连接的可靠的数据通信方式,即TCP套接字;
2)数据报套接字(Raw Sockets):使用了不面向连接的数据传输方式,即UDP套接字;
3)原始套接字(SOCK——RAW):没有经过处理的IP数据包,可以根据自己程序的要求进行封装。
2、常用函数
1、创建套接字函数:成功时返回文件描述符,失败时返回-1
int socket(int domain,int type,int protocol);
//参数domain用于指定创建套接字所使用的协议族(可取AF_UNIX,AF_INET,AF_INTE6)
//参数type指定套接字的类型(可取SOCK_STREAM,SOCK_DGRAM,SOCK_RAW)
//参数protocol通常设置为0
2、在指定套接字上创建链接函数:成功时返回0,失败时返回-1
int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);
//参数sockfd是一个由函数socket创建的套接字
//参数serv_addr是一个地址结构,指定服务器的IP地址和端口号
//参数addrlen为参数serv_addr的长度
3、将一个套接字和某个端口绑定在一起的函数:成功时返回0,失败时返回-1
int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen);
//一般只有服务器端的程序调用,参数my_addr指定了sockfd将绑定到的本地
//地址,可以将参数my_addr的sin_addr设置为INADDR_ANY而不是某个确定
//IP地址就可以绑定到任何网络接口。
4、把套接字转化为被动监听函数:成功时返回0,失败时返回-1
int listen(int s,int backlog);
//参数s为套接字,参数backlog指定链接请求队列的最大长度;
5、接收连接请求函数:成功时返回文件描述符,失败时返回-1
int accept(int s,struct sockaddr *addr,socklen_t *addrlen);
//参数s是由函数socket创建,经函数bind绑定到本地某一端口上,然后通过
//函数listen转化而来的监听套接字
//参数addr用来保存发起连接请求的主机的地址和端口
//参数addrlen是addr所指向的结构体的大小
6、在TCP套接字上发送数据函数:有连接
包含3要素:套接字s,待发数据msg,数据长度len
ssize_t send(int s,const void *msg,size_t len,int flags);
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0
7、在TCP套接字上接收数据函数:有连接
包含3要素:套接字s,接收缓冲区buf,长度len
ssize_t recv(int s,void *buf,size_t len,int flags);
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0
8、在UCP套接字上发送数据函数:无连接
ssize_t sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
//函数功能与函数send类似,但函数sendto不需要套接字处于连接状态,所以
//该函数通常用来发送UDP数据,同时因为是无连接的套接字,在使用sendto时
//需要指定数据的目的地址,参数msg指向待发送数据的缓冲区。
//参数len指定了待发送数据的长度
//参数flags是控制选项,含义与send函数中的一致
//参数to用于指定目的地址,目的地址的长度由tolen指定
9、在UDP套接字上接收数据函数:无连接
ssize_t recvfrom(int s ,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
//与函数recv功能类似,只是函数recv只能用于面向连接的套接字,而函数
//recvfrom没有此限制,可以用于从无连接的套接字上接收数据
//参数buf指向接收缓冲区
//参数len指定了缓冲区的大小
//参数flags是控制选项,含义与recv中的一致
//如果参数from非空,且该套接字不是面向连接的,则函数recvfrom返回时,
//参数from中将保存数据的源地址
//参数fromlen在调用recvfrom前为参数from的长度,调用recvfrom后将
//保存from的实际大小
10、关闭套接字函数:
int close(int fd);
//参数fd为一个套接字描述符;
11、多路复用函数:
int select(int n,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
//参数n是需要监视的文件描述符数
//参数readfds指定需要监视的可读文件描述符集合
//参数writefds指定需要监视的可写文件描述符集合
//参数exceptfds指定需要监视的异常文件描述符的集合
//参数timeout指定了阻塞的时间
3、服务器端套接字(接收连接请求的套接字)创建过程
第一步:调用socket函数创建套接字。
第二步:调用bind函数分配IP地址和端口号。
第三部:调用listen函数转为可接收请求状态。
第四步:调用accept函数受理连接请求。
4、客户端套接字(发送连接请求的套接字)创建过程
只有两步:
1、调用socket函数创建套接字。
2、调用connect函数向服务器端发送连接请求。
5、Linux的文件操作
文件描述符:是系统自动分配给文件或套接字的整数。
每当生成文件或套接字,操作系统就会自动返回给我们一个整数。这个整数就是文件描述符,即创建的文件或套接字的别名,方便称呼而已。文件描述符在Windows中又称为句柄。
1、打开文件:
2、关闭文件或套接字:
3、将数据写入文件:
4、读取文件中的数据:
注:ssize_t = signed int, size_t = unsigned int
,都是通过typedef声明,为基本数据类型取的别名。既然已经有了基本数据类型,那么为什么还需要为它取别名呢?是因为目前普遍认为int是32位的,而过去16位操作系统时代,int是16位的。根据系统的不同,时代的变化,基本数据类型的表现形式也随着变化的。如果为基本数据类型取了别名,以后要修改,也就只需要修改typedef声明即可,这将大大减少代码变动。
6、服务器端实例
服务器端受理连接请求的程序。编译并运行该程序,创建等待连接请求的服务器端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, const char * argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if(argc != 2)
{
printf("Usage:%s <port>\n", argv[0]);
exit(1);
}
//(1)调用socket函数创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//(2)调用bind函数分配IP地址和端口号
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
//(3)调用listen函数转为可接收请求状态
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
//(4)调用accept函数受理连接请求.没有连接请求时调用不会返回,直到有请求
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
//(5)write函数用于传输数据。执行到此行说明已有了连接请求
//向文件描述符为clnt_sock的客户端文件中传输message中的数据
write(clnt_sock, message, sizeof(message));
close(clnt_sock);//(6)
close(serv_sock);//(7)
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc(‘\n‘, stderr);
exit(1);
}
7、客户端实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, const char * argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
//(1)创建套接字.但此时并不马上分为客户端或服务器端
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//(2)调用connect函数向服务器端发送连接请求.此时确定为客户端
if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
//(3)调用read函数向自身声明的message数组中保存数据
str_len = read(sock, message, sizeof(message) - 1);
if(str_len == -1)
error_handling("read() error");
printf("Message from server: %s \n", message);
close(sock);//(4)
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc(‘\n‘, stderr);
exit(1);
}
参考:《TCP/IP网络编程》