Socket套接字编程
前天面试了环信公司,在面试的时候,感觉面试官很亲切,临走的时候叫我回去再把TCP和UDP网络编程在看看,并且给他发过去,我知道现在的水平确实还有很大提升的空间,我也会一直不断的学习,加强自身的知识水平建设。
(1)基于UDP协议的socket套接字编程
UDP协议是非链接的协议,它不与对方建立连接,而是直接把要发送的数据发送给对方。所以UDP协议适用于一次传输数据量很少,对可靠性要求不高的应用场景。但是正是因为udp协议没有类似于tcp的三次握手,可靠传输机制等,所以效率比较高。
UDP协议的应用也非常广泛,比如知名的应用层协议:SNMP,DNS都是基于UDP的。
UDP通信的流程比较简单,因此要搭建这么一个常用的UDP通信框架也是比较简单的。以下是UDP的框架图。
由以上框图可以看出,客户端要发起一次请求,仅仅需要个步骤(socket和sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。
- 1.socket()函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
domain 如果是IPv4就写AF_INET,如果是IPV6就是AF_INET6.
-
type 如果是TCP就写SOCK_STREAM,UDP就写SOCK_DGRAM。
-
protocol 填0就可以了。因为通过前面两个参数一般就可以确定后面的这个是什么类型,前面填了AF_INET,SOCK_DGRAM 适用的就是UDP了。
-2. bind()函数
这里服务器调用就可以了,将本机IP地址,选定的一个端口与前面的套接字绑定。客户端的话,就可以不用了。
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
如果bind()返回成功,就会返回0。
如果返回不成功,可以调用GetLastError() 函数,通过错误编码查看是什么错误。
- 数据的接收和发送
UDP在服务器绑定地址后,就可以直接发送接受消息了。当然客户端创建成功套接字后也就可以直接接受和发送了。 一般使用sendto(),recvfrom()
.
int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
- s: socket描述符。
- buf: UDP数据报缓存地址。
- len: UDP数据报长度。
- flags: 该参数一般为0。
- to: sendto()函数参数,struct sockaddr_in类型,指明UDP数据发往哪里报。
- tolen: 对方地址长度,一般为:sizeof(struct sockaddr_in)。
- from: recvfrom()函数参数,struct sockaddr_in类型,指明从哪里接收UDP数据报。
- fromlen: 对方地址长度,一般为:sizeof(struct sockaddr_in)。
- 函数返回值
1.对于sendto()函数,成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
2.对于recvfrom()函数,成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中。
以下是UDP网络编程的一个小栗子。
server.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
//服务器程序,./server [ip] [port]
// ./server 127.0.0.1 9090
int main(int argc,const char* argv[])
{
if(argc < 3)
{
printf("input error!\n");
return 1;
}
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd < 0)
{
perror("socket\n");
return 2;
}
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
int bd = bind(fd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(bd < 0)
{
perror("bind\n");
return 3;
}
char buf[1024] = {0};
struct sockaddr_in client_addr;
while(1)
{
socklen_t len = sizeof(client_addr);
ssize_t s = recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&client_addr,&len);
if(s < 0)
{
perror("recvfrom");
continue;
}
buf[s] = '\n';
printf("[%s]: [%d]: %s",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buf);
sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&client_addr,sizeof(client_addr));
}
close(fd);
return 0;
}
/////////////////////////////////////////////////////////////////////////////
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
// ./client 127.0.0.1 9090
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd < 0)
{
perror("socket\n");
return 1;
}
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buf[1024] = {0};
struct sockaddr_in peer;
while(1)
{
socklen_t len = sizeof(peer);
printf(">>>");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s < 0)
{
perror("read\n");
return 1;
}
buf[s-1] = '\n';
sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
ssize_t rc = recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
if(rc > 0)
{
buf[rc] = '\n';
printf("server echo: %s\n",buf);
}
}
close(fd);
return 0;
}
TCP套接字编程
主要函数:
1.创建套接字
int socket(int domain,int type,int protocol);
//domain:该参数一般被设置为AF_INET,表示使用的是IPv4地址。还有更多选项可以利用man查看该函数
//type:该参数也有很多选项,例如SOCK_STREAM表示面向流的传输协议,SOCK_DGRAM表示数据报,我们这里实现的是TCP,因此选用SOCK_STREAM,如果实现UDP可选SOCK_DGRAM
//protocol:协议类型,一般使用默认,设置为0
2.绑定
int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//sockfd:服务器打开的sock
//后两个参数可以参考第四部分的介绍
3.监听
int listen(int sockfd,int backlog);
//sockfd的含义与bind中的相同。
//backlog参数解释为内核为次套接口排队的最大数量,这个大小一般为5~10,不宜太大(是为了防止SYN攻击)
4.接受连接
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//addrlen是一个传入传出型参数,传入的是调用者的缓冲区cliaddr的长度,以避免缓冲区溢出问题;传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用accept()返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有accept的客户端就处于连接等待状态。
三次握手完成后,服务器调用accept()接收连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
5、请求连接
int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
TCP连接的基本流程:
服务器:首先调用socket()创建一个套接字用来通讯,其次调用bind()进行绑定这个文件描述符,并调用listen()用来监听端口是否有客户端请求来,如果有,就调用accept()进行连接,否则就继续阻塞式等待直到有客户端连接上来。连接建立后就可以进行通信了。
客户端:调用socket()分配一个用来通讯的端口,接着就调用connect()发出SYN请求并处于阻塞等待服务器应答状态,服务器应答一个SYN-ACK分段,客户端收到后从connect()返回,同时应答一个ACK分段,服务器收到后从accept()返回,连接建立成功。客户端一般不调用bind()来绑定一个端口号,并不是不允许bind(),服务器也不是必须要bind()。
为什么不建议客户端进行bind()?
答:当客户端没有自己进行bind时,系统随机分配给客户端一个端口号,并且在分配的时候,操作系统会做到不与现有的端口号发生冲突。但如果自己进行bind,客户端程序就很容易出现问题,假设在一个PC机上开启多个客户端进程,如果是用户自己绑定了端口号,必然会造成端口冲突,影响通信。
TCP通信的一个例子:
//server.c
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<string.h>
// ./server 127.0.0.1 8080
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage: ./server [ip] [port]\n");
return 1;
}
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("sock");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
int bd = bind(sockfd,(struct sockaddr*)&server,sizeof(server));
if(bd < 0)
{
perror("bind");
return 1;
}
int ls = listen(sockfd,5);
if(ls < 0)
{
perror("listen");
return 1;
}
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_fd = accept(sockfd,(struct sockaddr*)&client,&len);
if(new_fd < 0)
{
perror("accept");
continue;
}
while(1)
{
char buf[1024] = {0};
ssize_t read_size = read(new_fd,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
continue;
}
if(read_size == 0)
{
printf("client close!\n");
break;
}
buf[read_size] = '\0';
printf("[%s:%d]:%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
write(new_fd,buf,strlen(buf));
}
close(new_fd);
}
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////////
//client.c
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage: ./client [ip] [port]\n");
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(fd,(struct sockaddr*)&server,sizeof(server));
if(ret < 0)
{
perror("connect");
return 1;
}else
{
printf("connect success!\n");
}
while(1)
{
char buf[1024] = {0};
printf(">>>");
fflush(stdout);
int read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0)
{
perror("read");
continue;
}
if(read_size == 0)
{
printf("goodbye\n");
break;
}
buf[read_size] = '\0';
write(fd,buf,strlen(buf));
char buf_output[1024] = {0};
ssize_t size = read(fd,buf_output,sizeof(buf_output)-1);
if(size < 0)
{
perror("read");
continue;
}
if(size ==0)
{
printf("goodbye!");
break;
}
buf_output[size] = '\0';
printf("%s\n",buf_output);
}
close(fd);
return 0;
}
**一般而言,UDP和TCP编程会使用到的基本函数。**
// 创建socket 文件描述符(TCP/UDP, 客户端+ 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号(TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求(TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接(TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);