1、socket API 常用函数
这些函数都在sys/socket.h中。
1.1 socket()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)
type
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mecha-
nism may be supported.
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum
length; a consumer is required to read an entire packet with each input system call.
SOCK_RAW Provides raw network protocol access.
SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET Obsolete and should not be used in new programs; see packet(7).
注意:
- socket打开一个网络通讯端口,如果成功的话就像read一样返回一个文件描述符。
- 应用程序可以向读写文件一样用read/write在网络上收发数据。
- 如果socket调用出错则返回-1。
- 对于IPv4,family参数指定为AF_INET。
- 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
- protocol参数的介绍从略,指定为0即可。
1.2 bind()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数
When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind()
assigns the address specified to by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size,
in bytes, of the address structure pointed to by addr. Traditionally, this operation is called “assigning a name to a socket”.
It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive connections (see accept(2)).
The rules used in name binding vary between address families. Consult the manual entries in Section 7 for detailed information. For AF_INET see ip(7), for AF_INET6 see ipv6(7), for AF_UNIX see unix(7), for AF_APPLETALK see ddp(7), for AF_PACKET see packet(7), for AF_X25 see x25(7) and for AF_NETLINK see netlink(7).
注意:
- 服务器程序所监听的⺴络地址和端⼝号通常是固定不变的,客户端程序得知服务器程序的地址和端⼝号后就可以向服务器发起连接; 服务器需要调⽤bind绑定⼀个固定的⺴络地址和端⼝号;
- bind()成功返回0,失败返回-1。
- bind()的作⽤是将参数sockfd和myaddr绑定在⼀起, 使sockfd这个⽤于网络通讯的⽂件描述符监听myaddr所描述的地址和端⼝号;
- struct sockaddr *是⼀个通⽤指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,⽽它们的⻓度各不相同,所以需要第三个参数addrlen指定结构体的⻓度;
1.3 listen()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数:
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).
The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.
注意:
- listen()声明socket处于监听状态,并且做多允许有backlog个客户端处于连接等待状态。
- listen()成功返回0,失败返回-1。
1.4 accept()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:socket文件描述符
addr:sockaddr类型结构体
addrlen:sockaddr类型结构体大小
注意:
- 三次握⼿完成后, 服务器调⽤accept()接受连接;
- 如果服务器调⽤accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是⼀个传出参数,accept()返回时传出客户端的地址和端⼝号;
- 如果给addr 参数传NULL,表⽰不关⼼客户端的地址;
- addrlen参数是⼀个传⼊传出参数(value-result argument), 传⼊的是调⽤者提供的, 缓冲区addr的⻓度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际⻓度(有可能没有占满调⽤者提供的缓冲区);
2、服务器端
2.1 基本步骤:
- 创建socket
- 绑定端口号
- 把socket转换成被动模式(listen)
- 循环的进行accept
- 从accept返回的new_fd之中读取客户端的请求
- 根据读到的请求进行计算和处理
- 把处理后的结果返回给客户端
2.2 源码:
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#define _PORT_ 9999
#define _BACKLOG_ 10
int main(){
//创建socket文件描述符(TCP 客户端 + 服务器)
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
printf("create socket error,error : %d , error string : %s \n",errno,strerror(errno));
}
//sockaddr结构(地址类型 端口号 IP地址)
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
//初始化server_socket
bzero(&server_socket,sizeof(server_socket));
server_socket.sin_family = AF_INET;
//INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或"所有地址"、"任意地址"。
server_socket.sin_addr.s_addr = htonl(INADDR_ANY);
server_socket.sin_port = htons(_PORT_);
//绑定端口号
if(bind(sock,(struct sockaddr*)&server_socket,sizeof(struct sockaddr_in)) < 0){
//绑定失败
printf("bind error,errno: %d , error string: %s \n",errno,strerror(errno));
close(sock);
return 1;
}
//开始监听socket
//_BACKLOG_ 最多可监听10个
if(listen(sock,_BACKLOG_) < 0){
printf("lisen error,errno: %d , error string: %s \n",errno,strerror(errno));
close(sock);
return 2;
}
printf("bind and listen success! wait accept...\n");
//循环接收客户端的请求
while(1){
socklen_t len = 0;
int client_sock = accept(sock,(struct sockaddr *)&client_socket,&len);
if(client_sock < 0){
printf("accept error,client ip: %s , error: %d , error string: %s \n",inet_ntoa(client_socket.sin_addr),errno,strerror(errno));
close(sock);
return 3;
}
//从accept返回的client_sock中读取客户端的请求
char buf_ip[INET_ADDRSTRLEN] = {0};
//将客户端IP地址转换成字符串存入缓冲区buf_ip中
inet_ntop(AF_INET,&client_socket.sin_addr,buf_ip,sizeof(buf_ip));
printf("[connect] ip: %s ,port: %d\n",buf_ip,ntohs(client_socket.sin_port));
while(1){
//根据读取到的请求进行计算和处理
//这里实现一个简单的回显服务器,对客户端请求不作任何处理,直接输出
char buf[1024] = {0};
read(client_sock,buf,sizeof(buf));
printf("client :# %s\n",buf);
printf("server :$ ");
//把处理后的结果发送给客户端
memset(buf,'\0',sizeof(buf));
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = '\0';
write(client_sock,buf,strlen(buf)+1);
printf("please wait...\n");
}
}
close(sock);
return 0;
}
3、客户端
3.1 基本步骤:
- 创建socket
- 和服务器建立连接
- 给服务器发送数据
- 从服务器读取返回的结果
- 和服务器断开连接
3.2 源码:
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<string.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVER_PORT 9999
#define SERVER_IP "192.168.11.128"
int main(int argc,char *argv[]){
if(argc != 2){
printf("Usage:client IP\n");
return 1;
}
char *str = argv[1];
char buf[1024];
memset(buf,'\0',sizeof(buf));
struct sockaddr_in server_sock;
int sock = socket(AF_INET,SOCK_STREAM,0);
bzero(&server_sock,sizeof(server_sock));
server_sock.sin_family = AF_INET;
//字符串SERVER_IP转成in_addr server_sock
inet_pton(AF_INET,SERVER_IP,&server_sock.sin_addr);
server_sock.sin_port = htons(SERVER_PORT);
int ret = connect(sock,(struct sockaddr *)&server_sock,sizeof(server_sock));
if(ret<0){
printf("connect failed ...,errno is : %d,errstring is: %s\n",errno,strerror(errno));
return 1;
}
printf("connect success...\n");
while(1){
printf("client:#");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = '\0';
write(sock,buf,sizeof(buf));
if(strncasecmp(buf,"quit",4) == 0){
printf("quit!\n");
break;
}
printf("please wait ...\n");
read(sock,buf,sizeof(buf));
printf("server :$ %s\n",buf);
}
close(sock);
return 0;
}
3.3 注意:
- 由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。
- 客户端没有必要非要绑定端口号,否则如果在一台机器上启动多个客户端,就会出现端口号被占用,导致不能正确建立连接。
- 服务器端如果不绑定端口号的话,内核会给服务器端自动分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇见麻烦。
- 客户端需要调用connect()连接服务器。connect和bind的参数形式一样,区别在于bind的参数是自己的地址,connect的参数是对方的地址。
- connect调用失败的几个可能的原因:服务器没有关闭防火墙,服务器没启动、地址错误、端口错误、类型错误等。
4、程序运行结果
服务器:
客户端:
查看监听状态:
5、缺陷
5.1 测试多个连接的情况
客户端1:
客户端2:
服务器:
这时我们发现第二个客户端并不能正确的和服务器建立连接。
5.2 原因分析:
是因为我们accecpt了⼀个请求之后, 就在⼀直while循环尝试read, 没有继续调⽤到accecpt, 导致不能接受新的请求。
我们当前的这个TCP, 只能处理⼀个连接。
改进方法:
想要服务器可以处理多个客户端的请求,可以依靠多线程/多进程/生产者消费者模型来实现。