一、socket概述
socket计算机中的一种网络传输机制,对TCP和UDP的封装,自动帮我们在底层完成各种协议操作,接收到数据包后返回到上层应用。
socket分为客户端和服务端,它的工作模型为:
二、socket 网络地址
2.1 网络字节序
关于字节序的概念可以查看计算机中的字节序。
一般来说,计算机是低字节序,网络传输是高字节序,两者之间并不统一。使用时需要通过以下函数进行转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
记忆方式
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如端口号是16位(s),主机(h)转网络(n)字节序的函数为htons
。
2.2 IP地址转换函数
socket地址
计算机中的IP地址是一个32位长整数,因为ip地址最多为255.255.255.255
,每个点位最多占1个字节=8位,所以IP地址为32位整数。
我们用的地址是一个sockaddr
类型,它包含了地址族,端口号和IP地址等信息。不过它是很早以前的地址结构了,为了适应需要,现在衍生出了sockaddr_in
等地址类型如下图所示。
但是为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。类型的定义如下:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr''. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
/* Internet address. */
struct in_addr {
__be32 s_addr;
};
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */
__be32 sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
struct in6_addr {
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
一般我们常用的是sockaddr_in
,简单用法为:
sockaddr_in clnt_addr;
clnt_addr.sin_family = AF_INET;
clnt_addr.sin_port = htons(9999);
clnt_addr.s_addr.sin_addr = INADDR_ANY;
地址转换函数
早期IPv4地址转换函数:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
在后期引入IPv6后的转换函数支持IPv4和IPv6:
#include <arpa/inet.h>
// 字符串类型地址转换成整形
int inet_pton(int af, const char *src, void *dst);
// 整形地址转换成字符串类型
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
其中af
是地址族,一般填写AF_INET
表示以太网。
三、socket操作函数
3.1 socket
socket函数用于创建一个socket对象,在linux环境中,socket也是一个文件,因此该函数实际返回的是一个文件描述符。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明
domain:
- AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址。
- AF_INET6 与上面类似,不过是来用IPv6的地址。
- AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。
type:
- SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。。
- SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。。
- SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取
- SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)。
- SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
- 0 默认协议
返回值:
成功返回一个新的文件描述符,失败返回-1,设置errno。
3.2 bind
bind用于绑定地址到socket。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
sockfd: socket文件描述符。
addr: 构造出IP地址加端口号。
addrlen: sizeof(addr)长度。
返回值: 成功返回0,失败返回-1, 设置errno。
绑定前要先设置好地址:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);
3.3 listen
listen用于监听某个端口号:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明
sockfd: socket文件描述符。
backlog: 排队建立3次握手队列和刚刚建立3次握手队列的链接数和。
返回值: listen()成功返回0,失败返回-1。
查看系统默认backlog
> cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
3.4 accept
服务端接受一个socket连接,此时的连接已经三次握手完成
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
**sockdf: socket文件描述符。
**addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号。
**addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小。
**返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno。
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-resultargument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
3.5 connect
客户端连接服务端的函数,此时开始三次握手。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockdf: socket文件描述符。
addr: 传入参数,指定服务器端地址信息,含IP地址和端口号。
addrlen: 传入参数,传入sizeof(addr)大小。
返回值:成功返回0,失败返回-1,设置errno。
3.6 recv和recvfrom
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
recv用于TCP协议读取数据:
sockfd: 连接的socket文件描述符。
buf: 用来保存读取数据的变量。
len: 读取的数据大小
flags: 读取标志,一般设置为0。
返回值:成功返回读取到的字节数,失败返回-1,0表示已经断开连接。
recv用于udp协议,参数含义类似。
由于socket也是一个文件描述符,因此也可以使用read来读取socket中的数据。
3.7 send和sendto
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
send和sendto分别用于tcp和udp协议,参数含义和上面的recv类似,也可以使用write向socket中些数据。
四、一个简单的服务端和客户端案例
以下是一个示例demo,客户端在连接上服务端后输入相应的字符串发送过去,然后服务端把所有字符转成大写返回。
client.c
#include<sys/types.h>
#include<sys/socket.h>
#include<errno.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
const int SERVER_PORT = 8080;
const int MAX_BUF_SIZE = 1024;
const char HOST[] = "127.0.0.1";
int main(int argc, char** argv){
if (argc < 3){
printf("Usage: ./client host port");
return 0;
}
int clnt_fd, n;
struct sockaddr_in serv_addr;
char send_buf[MAX_BUF_SIZE];
clnt_fd = socket(AF_INET, SOCK_STREAM, 0);
if (clnt_fd == -1){
perror("socket error");
return 0;
}
// set server addr
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET, argv[1], &serv_addr.sin_addr);
if (connect(clnt_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
perror("connect error");
return 0;
}
printf("connect to server success!\n");
int index = 0;
while (index<100){
bzero(send_buf, MAX_BUF_SIZE);
scanf("%s", send_buf);
if(strcmp("exit", send_buf) == 0){
break;
}
n = write(clnt_fd, send_buf, strlen(send_buf));
if (n == -1){
perror("write error");
return 0;
}
n = read(clnt_fd, send_buf, n);
if (n == -1){
perror("read error");
return 0;
}
printf("%s\n", send_buf);
}
close(clnt_fd);
return 0;
}
server.c
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<ctype.h>
#include<stdlib.h>
const int SERV_PORT = 8080;
const int MAX_CONN = 1;
const int MAX_BUF_SIZE = 1024;
const int MAX_IP_SIZE = 17;
void toUpper(char buf[]){
int i = 0;
while (i < MAX_BUF_SIZE && buf[i]){
buf[i] = toupper(buf[i]);
i++;
}
}
int main(int argc, char** argv){
if (argc < 2){
printf("./server port\n");
return 0;
}
int nPort = atoi(argv[1]);
int clnt_fd, serv_fd;
struct sockaddr_in clnt_addr, serv_addr;
socklen_t len = sizeof(serv_addr);
char buf[MAX_BUF_SIZE], ip[MAX_IP_SIZE];
int n;
serv_fd = socket(AF_INET, SOCK_STREAM, 0);
if (serv_fd == -1){
perror("Create socket error");
return 0;
}
bzero(&serv_addr, sizeof(serv_addr));
bzero(&clnt_addr, sizeof(clnt_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(nPort);
if (bind(serv_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
perror("Bind socket error");
return 0;
}
if (listen(serv_fd, MAX_CONN) == -1){
perror("Listen error");
return 0;
}
printf("server hs start and listen %d\n", nPort);
clnt_fd = accept(serv_fd, (struct sockaddr*)&serv_addr, &len);
if (clnt_fd == -1){
perror("Accpet error");
return 0;
}
inet_ntop(AF_INET, &serv_addr.sin_addr, ip, sizeof(serv_addr));
printf("Accept[%s:%d]\n", ip, serv_addr.sin_port);
bzero(buf, MAX_BUF_SIZE);
while((n = read(clnt_fd, buf, MAX_BUF_SIZE)) != 0){
if (n == -1){
perror("Read error");
continue;
}
printf("Receive %d byte data: %s\n", n, buf);
toUpper(buf);
write(clnt_fd, buf, n);
bzero(buf, MAX_BUF_SIZE);
}
printf("client[%s:%d] has disconnected!\n", ip, serv_addr.sin_port);
close(clnt_fd);
return 0;
}
运行结果
服务端通过./server 9988
启动,监听9988端口。
客户端连接端口输入字符串测试:
> ./client 127.0.0.1 9988
connect to server success!
helloworld
HELLOWORLD
client
CLIENT
maqian
MAQIAN
exit # 输入exit退出
服务端显示:
root@ma:/data/code/c/2-socket/test# ./server 9988
server hs start and listen 9988
Accept[127.0.0.1:50336]
Receive 10 byte data: helloworld
Receive 6 byte data: client
Receive 6 byte data: maqian
client[127.0.0.1:50336] has disconnected!
此处评论已关闭