**socket函数是一种可用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数
int socket(int Adress_family,int type,int protocol);
af:如AF_INET type:连接类型,通常是SOCK_STREAM或SOCK_DGRAM protocol:协议类型,通常是IPPROTO_TCP或IPPROTO_UDP 返回值:socket的编号,为-1表示失败
** 将一个地址和一个端口号绑定到一个socket连接上
int bind(int socket,sockaddr * address,uint addrlen);
socket:之前创建的socket, sockaddr:一个用来存放Ip地址和端口号的结构体,addrlen:此结构体的长度 返回值:为-1表示失败,若端口被占用,会从新绑定一个随机端口(仍返回失败)
**// UDP时:接收任何一个发送到该socket的消息(无法获取发送方地址)
**// TCP时:接收一个已连接的socket (connected socket)发送的信息
int recv(int socket,char * buf,uint buflen,int flag);【阻塞】
socket:UDP时,为之前创建的socket,TCP时,为connected socket,buf:接收的缓冲区,buflen:缓冲区的长度,flag:一般为0 返回值:>0表示收到的字节数,=0表示连接被关闭,-1表示出错
- 注意:对于TCP,请确保socket是已连接的,因为只有已连接的socket会阻塞此函数
- 该函数实际上是从缓冲区取指定长度的数据,如果缓冲区没有数据,则会阻塞;如果没有取完,则下次使用此函数的时候不会阻塞
- 应注意:当一次无法获得对方发送的全部数据,在数据不完整的时候,程序可能无法向下执行,可以考虑将数据放在缓冲区中,等数据全部接收完成的时候再使用
**将一个socket设置为监听状态,专门用来监听的socket叫做master socket
int listen(int socket,int maxconn);【仅TCP】【服务器端】
maxconn:最大接收连接数, 返回值:失败返回-1,成功返回0
**接收一个客户机的连接,返回一个socket,来自客户机的socket叫connected socket
int accept(int socket,sockaddr * fromaddr,int * addrlen);【阻塞】【仅TCP】【服务器】
socket:用来监听的socket(master socket),fromaddr:客户机的地址信息, addrlen:地址结构体的长度(输入输出参数) 返回值:返回一个新的socket,这个socket专门用来与此客户机通讯(connected socket)
**使用当前socket连接一个地址(与服务器建立正式连接),此函数会触发服务器端的accept、select函数
int connect(int socket,sockaddr * addr,int addrlen);【仅TCP】【客户端】
返回值:成功则返回0,失败返回非0,错误码GetLastError()。
**向一个已连接的socket发送信息,这个socket应该是connected socket(非master socket)
int send(int socket,char * buf,char buflen,int flag);【仅TCP】
**关闭一个已存在的socket【正常关闭】
int closesocket(int socket);
返回值:失败返回-1,成功返回0
初识TCP协议链接(单用户):在认识tcpserver之前,简单理解服务端和客户端是如何交流的,如下:
listenfd看成是服务端的“迎宾”,负责把对应的输入带进来;
connld看成是负责对输入的进行 *** 作的“服务员”;
并且要理解如listen,bind等 *** 作是利用内核的 *** 作,分配出资源等待连接,故只用进行一次即可。
对此结构图,利用基本的函数构建了第一个简单的tcpserver.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
} //这里创建一个迎宾,每来一个客户端,socket的正常输出+1:eg先是2,再来一个就会分配3
memset(&servaddr, 0, sizeof(servaddr));/*将指针变量servaddr所指向的前sizeof(servaddr)字
节的内存单元用一个“整数” 0 替换,注意0是 int 型。
&servaddr是 void* 型的指针变量,所以它可以为任
何类型的数据进行初始化。
*/
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示所有网卡
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
} //迎宾绑定端口
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}//迎宾监听端口
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
} //这里就是迎宾把任务带过来,判断是不是出错
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
close(connfd);
}
close(listenfd);
return 0;
}
在对其进行编译生成可执行文件,发现我们最后只能成功得到一个客户端发送的信息,并在服务器端进行 *** 作。
若第二个客户端对其进行连接请求,请求先在内核里的协议栈里进行三次握手,实现连接,但是服务器应用层并没有关系。
那么为了对更多用户机进行 *** 作,讨论了来一个客户端分配一个线程的思路:
利用线程实现多用户(用户少约C10K): 于是将处理的while(1)的优化成如下代码,并创建了一个函数,去把之前的whlie里面的任务放进去,分配一个线程。 但是没分配一个线程,内存的使用量也会增加,当超过最大使用内存时,会造成服务器重启。
void *client_routine(void *arg) { //
int connfd = *(int *)arg;
char buff[MAXLNE];
while (1) {
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
break;
}
}
return NULL;
}
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}//这里可以理解为,每一桌分配一个不一样的服务员connfd,以便区分是拿一个客户端发送的信息;
//但是,没有合适的组件去查找,到底是具体的客户端发送的信息
pthread_t threadid; //unsigned long int
pthread_create(&threadid, NULL, client_routine, (void*)&connfd);//来一个分配一个
}
除此之外,我们可以发现,每当我们accept一个client,我们就分配一个不同的服务员,这样不利于我们对每一个客户端进行准确服务(这里的理解不是很准确),于是我们可以利用一个特殊组件:select;
利用(select组件)实现多用户(一个selectC10K,与线程复用selectC1000K ): select能够同时监控输入rset和输出wset,通过对客户端fd的管理,能够是一次能够处理的客户端数量增加很多,达到一个更好的服务数量。
fd_set rfds, rset, wfds, wset;//实际上就是我们监听的一个个集合(bool数组)
FD_ZERO(&rfds);//清空所有fd,初始化为0
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1) {
rset = rfds;
wset = wfds;
int nready = select(max_fd+1, &rset, &wset, NULL, NULL);
//max_fd+1是之后set中的最大fd容量加1
//int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout
if (FD_ISSET(listenfd, &rset)) { //可读为非0
//如果这次listenfd不存在,则放进来(实则就是有新客户端连接)
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds);
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
int i = 0;
for (i = listenfd+1;i <= max_fd;i ++) {
if (FD_ISSET(i, &rset)) { //
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
FD_SET(i, &wfds);
//reactor
//send(i, buff, n, 0);
} else if (n == 0) { //当客户端调用close的时候返回0
FD_CLR(i, &rfds);//清除fd,
//printf("disconnect\n");
close(i);
}
if (--nready == 0) break;
} else if (FD_ISSET(i, &wset)) {
send(i, buff, n, 0);
FD_SET(i, &rfds);
}
}
}
这里的select有上限主要是因为每执行一次select的时候,rset和wset都要被拿到内核里面去处理,对于大量fd管理,还是有一定局限性。
poll相当于每隔一段时间去检查客户端状态的结构体的集合。
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
int i = 0;
for (i = 1;i < POLL_SIZE;i ++) {
fds[i].fd = -1;
}
while (1) {
int nready = poll(fds, max_fd+1, -1);
//fds是结构体pollfd的数组,nfds用来标记结构体的总数,是poll函数调用阻塞的时间,单位:毫秒;
if (fds[0].revents & POLLIN) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept \n");
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
//int i = 0;
for (i = listenfd+1;i <= max_fd;i ++) {
if (fds[i].revents & POLLIN) {
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
} else if (n == 0) { //
fds[i].fd = -1; //清空fds
close(i);
}
if (--nready == 0) break;
}
}
}
利用(epoll组件)实现多用户:
epoll是由poll、select的一个函数改成了三个函数:
epoll_create:快递员 epoll_ctr(ADD,DEL,MOD):搬出搬进用户 epoll_wait:
epoll\select\poll都是事件驱动,只是管理的方式不同而已。
int epfd = epoll_create(1); //这里的参数大于0就行
struct epoll_event events[POLL_SIZE] = {0}; //设置快递员的袋子大小
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0;i < nready;i ++) {
int clientfd = events[i].data.fd;
if (clientfd == listenfd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) {//判断是否进行读的操作
printf("recv\n");
n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, n, 0);
} else if (n == 0) { //清楚 *** 作
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)